@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,122 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ConflictError, NotFoundError } from '../domain/errors.js';
|
|
3
|
+
import { SiteSchema } from '../domain/site.js';
|
|
4
|
+
const SITE_COLUMNS = 'id, hostname, upstream_id, tls_mode, created_at, updated_at';
|
|
5
|
+
/**
|
|
6
|
+
* `code` value SQLite reports for the per-column UNIQUE violation we care
|
|
7
|
+
* about (the `sites.hostname` unique index).
|
|
8
|
+
*/
|
|
9
|
+
const SQLITE_CONSTRAINT_UNIQUE = 'SQLITE_CONSTRAINT_UNIQUE';
|
|
10
|
+
/**
|
|
11
|
+
* `code` value SQLite reports when the `sites.upstream_id` foreign key points
|
|
12
|
+
* at an upstream that does not exist.
|
|
13
|
+
*/
|
|
14
|
+
const SQLITE_CONSTRAINT_FOREIGNKEY = 'SQLITE_CONSTRAINT_FOREIGNKEY';
|
|
15
|
+
function isSqliteError(err) {
|
|
16
|
+
return err instanceof Error && typeof err.code === 'string';
|
|
17
|
+
}
|
|
18
|
+
export class SiteRepository {
|
|
19
|
+
db;
|
|
20
|
+
#insertStmt;
|
|
21
|
+
#findByIdStmt;
|
|
22
|
+
#findByHostnameStmt;
|
|
23
|
+
#listStmt;
|
|
24
|
+
#updateStmt;
|
|
25
|
+
#deleteStmt;
|
|
26
|
+
constructor(db) {
|
|
27
|
+
this.db = db;
|
|
28
|
+
this.#insertStmt = db.prepare(`INSERT INTO sites (${SITE_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
29
|
+
this.#findByIdStmt = db.prepare(`SELECT ${SITE_COLUMNS} FROM sites WHERE id = ?`);
|
|
30
|
+
this.#findByHostnameStmt = db.prepare(`SELECT ${SITE_COLUMNS} FROM sites WHERE hostname = ?`);
|
|
31
|
+
this.#listStmt = db.prepare(`SELECT ${SITE_COLUMNS} FROM sites ORDER BY hostname ASC`);
|
|
32
|
+
this.#updateStmt = db.prepare('UPDATE sites SET hostname = ?, upstream_id = ?, tls_mode = ?, updated_at = ? WHERE id = ?');
|
|
33
|
+
this.#deleteStmt = db.prepare('DELETE FROM sites WHERE id = ?');
|
|
34
|
+
}
|
|
35
|
+
create(input) {
|
|
36
|
+
const id = randomUUID();
|
|
37
|
+
const now = new Date().toISOString();
|
|
38
|
+
try {
|
|
39
|
+
this.#insertStmt.run(id, input.hostname, input.upstreamId, input.tlsMode, now, now);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
this.#translateConstraintError(err, input);
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
return SiteSchema.parse({
|
|
46
|
+
id,
|
|
47
|
+
hostname: input.hostname,
|
|
48
|
+
upstreamId: input.upstreamId,
|
|
49
|
+
tlsMode: input.tlsMode,
|
|
50
|
+
createdAt: now,
|
|
51
|
+
updatedAt: now,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
findById(id) {
|
|
55
|
+
const row = this.#findByIdStmt.get(id);
|
|
56
|
+
return row ? this.#rowToEntity(row) : null;
|
|
57
|
+
}
|
|
58
|
+
findByHostname(hostname) {
|
|
59
|
+
const row = this.#findByHostnameStmt.get(hostname);
|
|
60
|
+
return row ? this.#rowToEntity(row) : null;
|
|
61
|
+
}
|
|
62
|
+
list() {
|
|
63
|
+
const rows = this.#listStmt.all();
|
|
64
|
+
return rows.map((row) => this.#rowToEntity(row));
|
|
65
|
+
}
|
|
66
|
+
update(id, patch) {
|
|
67
|
+
const existing = this.#findByIdStmt.get(id);
|
|
68
|
+
if (!existing) {
|
|
69
|
+
throw new NotFoundError(`Site with id "${id}" was not found`);
|
|
70
|
+
}
|
|
71
|
+
const now = new Date().toISOString();
|
|
72
|
+
const next = {
|
|
73
|
+
id: existing.id,
|
|
74
|
+
hostname: patch.hostname ?? existing.hostname,
|
|
75
|
+
upstream_id: patch.upstreamId ?? existing.upstream_id,
|
|
76
|
+
tls_mode: patch.tlsMode ?? existing.tls_mode,
|
|
77
|
+
created_at: existing.created_at,
|
|
78
|
+
updated_at: now,
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
this.#updateStmt.run(next.hostname, next.upstream_id, next.tls_mode, next.updated_at, id);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
this.#translateConstraintError(err, {
|
|
85
|
+
hostname: next.hostname,
|
|
86
|
+
upstreamId: next.upstream_id,
|
|
87
|
+
tlsMode: next.tls_mode,
|
|
88
|
+
});
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
return this.#rowToEntity(next);
|
|
92
|
+
}
|
|
93
|
+
delete(id) {
|
|
94
|
+
const result = this.#deleteStmt.run(id);
|
|
95
|
+
return result.changes > 0;
|
|
96
|
+
}
|
|
97
|
+
#rowToEntity(row) {
|
|
98
|
+
return SiteSchema.parse({
|
|
99
|
+
id: row.id,
|
|
100
|
+
hostname: row.hostname,
|
|
101
|
+
upstreamId: row.upstream_id,
|
|
102
|
+
tlsMode: row.tls_mode,
|
|
103
|
+
createdAt: row.created_at,
|
|
104
|
+
updatedAt: row.updated_at,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
#translateConstraintError(err, input) {
|
|
108
|
+
if (!isSqliteError(err))
|
|
109
|
+
return;
|
|
110
|
+
if (err.code === SQLITE_CONSTRAINT_UNIQUE) {
|
|
111
|
+
throw new ConflictError(`A site with hostname "${input.hostname}" already exists`, {
|
|
112
|
+
cause: err,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (err.code === SQLITE_CONSTRAINT_FOREIGNKEY) {
|
|
116
|
+
throw new NotFoundError(`Referenced upstream "${input.upstreamId}" does not exist`, {
|
|
117
|
+
cause: err,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=site-repository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site-repository.js","sourceRoot":"","sources":["../../../src/server/repositories/site-repository.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import { type Upstream } from '../domain/upstream.js';
|
|
3
|
+
export declare class UpstreamRepository {
|
|
4
|
+
private readonly db;
|
|
5
|
+
private readonly insertUpstreamStmt;
|
|
6
|
+
private readonly insertTargetStmt;
|
|
7
|
+
private readonly selectUpstreamByIdStmt;
|
|
8
|
+
private readonly selectTargetsForUpstreamStmt;
|
|
9
|
+
private readonly selectAllUpstreamsStmt;
|
|
10
|
+
private readonly selectAllTargetsStmt;
|
|
11
|
+
private readonly updateUpstreamStmt;
|
|
12
|
+
private readonly deleteTargetsForUpstreamStmt;
|
|
13
|
+
private readonly deleteUpstreamStmt;
|
|
14
|
+
private readonly touchUpstreamStmt;
|
|
15
|
+
constructor(db: Database.Database);
|
|
16
|
+
create(input: Omit<Upstream, 'id' | 'createdAt' | 'updatedAt'>): Upstream;
|
|
17
|
+
findById(id: string): Upstream | null;
|
|
18
|
+
list(): Upstream[];
|
|
19
|
+
update(id: string, patch: Partial<Omit<Upstream, 'id' | 'createdAt' | 'updatedAt'>>): Upstream;
|
|
20
|
+
delete(id: string): boolean;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=upstream-repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upstream-repository.d.ts","sourceRoot":"","sources":["../../../src/server/repositories/upstream-repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAG3C,OAAO,EAAkB,KAAK,QAAQ,EAAuB,MAAM,uBAAuB,CAAC;AAkD3F,qBAAa,kBAAkB;IAYjB,OAAO,CAAC,QAAQ,CAAC,EAAE;IAX/B,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IAClC,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAC;IACxC,OAAO,CAAC,QAAQ,CAAC,4BAA4B,CAAC;IAC9C,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAC;IACxC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC;IACtC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,4BAA4B,CAAC;IAC9C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC;gBAEN,EAAE,EAAE,QAAQ,CAAC,QAAQ;IA+BlD,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,GAAG,QAAQ;IAgCzE,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAQrC,IAAI,IAAI,QAAQ,EAAE;IAclB,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,GAAG,WAAW,GAAG,WAAW,CAAC,CAAC,GAAG,QAAQ;IA6C9F,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;CAI5B"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ConflictError, NotFoundError } from '../domain/errors.js';
|
|
3
|
+
import { UpstreamSchema } from '../domain/upstream.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 rowToUpstream(row, targets) {
|
|
11
|
+
return UpstreamSchema.parse({
|
|
12
|
+
id: row.id,
|
|
13
|
+
name: row.name,
|
|
14
|
+
targets,
|
|
15
|
+
loadBalancer: row.load_balancer,
|
|
16
|
+
createdAt: row.created_at,
|
|
17
|
+
updatedAt: row.updated_at,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function targetRowToTarget(row) {
|
|
21
|
+
return {
|
|
22
|
+
host: row.host,
|
|
23
|
+
port: row.port,
|
|
24
|
+
weight: row.weight,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export class UpstreamRepository {
|
|
28
|
+
db;
|
|
29
|
+
insertUpstreamStmt;
|
|
30
|
+
insertTargetStmt;
|
|
31
|
+
selectUpstreamByIdStmt;
|
|
32
|
+
selectTargetsForUpstreamStmt;
|
|
33
|
+
selectAllUpstreamsStmt;
|
|
34
|
+
selectAllTargetsStmt;
|
|
35
|
+
updateUpstreamStmt;
|
|
36
|
+
deleteTargetsForUpstreamStmt;
|
|
37
|
+
deleteUpstreamStmt;
|
|
38
|
+
touchUpstreamStmt;
|
|
39
|
+
constructor(db) {
|
|
40
|
+
this.db = db;
|
|
41
|
+
this.insertUpstreamStmt = db.prepare('INSERT INTO upstreams (id, name, load_balancer, created_at, updated_at) VALUES (?, ?, ?, ?, ?)');
|
|
42
|
+
this.insertTargetStmt = db.prepare('INSERT INTO upstream_targets (upstream_id, host, port, weight, position) VALUES (?, ?, ?, ?, ?)');
|
|
43
|
+
this.selectUpstreamByIdStmt = db.prepare('SELECT id, name, load_balancer, created_at, updated_at FROM upstreams WHERE id = ?');
|
|
44
|
+
this.selectTargetsForUpstreamStmt = db.prepare('SELECT upstream_id, host, port, weight, position FROM upstream_targets WHERE upstream_id = ? ORDER BY position ASC');
|
|
45
|
+
this.selectAllUpstreamsStmt = db.prepare('SELECT id, name, load_balancer, created_at, updated_at FROM upstreams ORDER BY name ASC');
|
|
46
|
+
this.selectAllTargetsStmt = db.prepare('SELECT upstream_id, host, port, weight, position FROM upstream_targets ORDER BY upstream_id ASC, position ASC');
|
|
47
|
+
this.updateUpstreamStmt = db.prepare('UPDATE upstreams SET name = ?, load_balancer = ?, updated_at = ? WHERE id = ?');
|
|
48
|
+
this.deleteTargetsForUpstreamStmt = db.prepare('DELETE FROM upstream_targets WHERE upstream_id = ?');
|
|
49
|
+
this.deleteUpstreamStmt = db.prepare('DELETE FROM upstreams WHERE id = ?');
|
|
50
|
+
this.touchUpstreamStmt = db.prepare('UPDATE upstreams SET updated_at = ? WHERE id = ?');
|
|
51
|
+
}
|
|
52
|
+
create(input) {
|
|
53
|
+
const id = randomUUID();
|
|
54
|
+
const now = new Date().toISOString();
|
|
55
|
+
const insert = this.db.transaction(() => {
|
|
56
|
+
this.insertUpstreamStmt.run(id, input.name, input.loadBalancer, now, now);
|
|
57
|
+
input.targets.forEach((target, position) => {
|
|
58
|
+
this.insertTargetStmt.run(id, target.host, target.port, target.weight, position);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
insert();
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (isUniqueConstraintError(err)) {
|
|
66
|
+
throw new ConflictError('upstream violates a unique constraint', { cause: err });
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
return rowToUpstream({
|
|
71
|
+
id,
|
|
72
|
+
name: input.name,
|
|
73
|
+
load_balancer: input.loadBalancer,
|
|
74
|
+
created_at: now,
|
|
75
|
+
updated_at: now,
|
|
76
|
+
}, input.targets.map((t) => ({ host: t.host, port: t.port, weight: t.weight })));
|
|
77
|
+
}
|
|
78
|
+
findById(id) {
|
|
79
|
+
const row = this.selectUpstreamByIdStmt.get(id);
|
|
80
|
+
if (!row)
|
|
81
|
+
return null;
|
|
82
|
+
const targets = this.selectTargetsForUpstreamStmt.all(id).map(targetRowToTarget);
|
|
83
|
+
return rowToUpstream(row, targets);
|
|
84
|
+
}
|
|
85
|
+
list() {
|
|
86
|
+
const rows = this.selectAllUpstreamsStmt.all();
|
|
87
|
+
if (rows.length === 0)
|
|
88
|
+
return [];
|
|
89
|
+
const targetsByUpstream = new Map();
|
|
90
|
+
for (const row of this.selectAllTargetsStmt.all()) {
|
|
91
|
+
const bucket = targetsByUpstream.get(row.upstream_id) ?? [];
|
|
92
|
+
bucket.push(targetRowToTarget(row));
|
|
93
|
+
targetsByUpstream.set(row.upstream_id, bucket);
|
|
94
|
+
}
|
|
95
|
+
return rows.map((row) => rowToUpstream(row, targetsByUpstream.get(row.id) ?? []));
|
|
96
|
+
}
|
|
97
|
+
update(id, patch) {
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
const apply = this.db.transaction(() => {
|
|
100
|
+
const existing = this.selectUpstreamByIdStmt.get(id);
|
|
101
|
+
if (!existing) {
|
|
102
|
+
throw new NotFoundError(`upstream not found: ${id}`);
|
|
103
|
+
}
|
|
104
|
+
const nextName = patch.name ?? existing.name;
|
|
105
|
+
const nextLoadBalancer = patch.loadBalancer ?? existing.load_balancer;
|
|
106
|
+
if (patch.name !== undefined || patch.loadBalancer !== undefined) {
|
|
107
|
+
this.updateUpstreamStmt.run(nextName, nextLoadBalancer, now, id);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
this.touchUpstreamStmt.run(now, id);
|
|
111
|
+
}
|
|
112
|
+
if (patch.targets !== undefined) {
|
|
113
|
+
this.deleteTargetsForUpstreamStmt.run(id);
|
|
114
|
+
patch.targets.forEach((target, position) => {
|
|
115
|
+
this.insertTargetStmt.run(id, target.host, target.port, target.weight, position);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
apply();
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (err instanceof NotFoundError) {
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
if (isUniqueConstraintError(err)) {
|
|
127
|
+
throw new ConflictError('upstream violates a unique constraint', { cause: err });
|
|
128
|
+
}
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
const fresh = this.findById(id);
|
|
132
|
+
if (!fresh) {
|
|
133
|
+
throw new NotFoundError(`upstream not found after update: ${id}`);
|
|
134
|
+
}
|
|
135
|
+
return fresh;
|
|
136
|
+
}
|
|
137
|
+
delete(id) {
|
|
138
|
+
const result = this.deleteUpstreamStmt.run(id);
|
|
139
|
+
return result.changes > 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=upstream-repository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upstream-repository.js","sourceRoot":"","sources":["../../../src/server/repositories/upstream-repository.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the NGINX binary path used by the validator.
|
|
3
|
+
*
|
|
4
|
+
* Reads `ZOOMIES_NGINX_BIN` on every call (no module-load caching) so tests
|
|
5
|
+
* can flip the value via `vi.stubEnv` between cases. Falls back to the
|
|
6
|
+
* conventional `/usr/sbin/nginx` install location.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getNginxBinary(): string;
|
|
9
|
+
//# sourceMappingURL=nginx-binary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nginx-binary.d.ts","sourceRoot":"","sources":["../../../src/server/validator/nginx-binary.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the NGINX binary path used by the validator.
|
|
3
|
+
*
|
|
4
|
+
* Reads `ZOOMIES_NGINX_BIN` on every call (no module-load caching) so tests
|
|
5
|
+
* can flip the value via `vi.stubEnv` between cases. Falls back to the
|
|
6
|
+
* conventional `/usr/sbin/nginx` install location.
|
|
7
|
+
*/
|
|
8
|
+
export function getNginxBinary() {
|
|
9
|
+
return process.env.ZOOMIES_NGINX_BIN ?? '/usr/sbin/nginx';
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=nginx-binary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nginx-binary.js","sourceRoot":"","sources":["../../../src/server/validator/nginx-binary.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,iBAAiB,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result of running `nginx -t` against a candidate config fragment.
|
|
3
|
+
*
|
|
4
|
+
* `ok` is the only field a caller usually needs — the others are kept so the
|
|
5
|
+
* reload orchestrator (Phase 5) can surface the underlying NGINX message to
|
|
6
|
+
* the operator on failure.
|
|
7
|
+
*/
|
|
8
|
+
export interface ValidationResult {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
exitCode: number | null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validates an NGINX config fragment with `nginx -t`.
|
|
16
|
+
*
|
|
17
|
+
* Strategy: write the fragment to a private temp dir, wrap it in a minimal
|
|
18
|
+
* `events {} http {}` shell, then invoke the NGINX binary on the wrapper.
|
|
19
|
+
* The wrapper uses an **absolute** include path — NGINX's `include`
|
|
20
|
+
* resolves relative paths against its own config-prefix, which we don't
|
|
21
|
+
* control.
|
|
22
|
+
*
|
|
23
|
+
* This function is side-effect-free from the caller's POV: it never mutates
|
|
24
|
+
* application state, never reloads NGINX, and always cleans its temp dir
|
|
25
|
+
* (cleanup errors are swallowed so they cannot mask the validation result).
|
|
26
|
+
* Phase 5 owns deciding what to do with a failed result.
|
|
27
|
+
*/
|
|
28
|
+
export declare function validateConfig(candidate: string): Promise<ValidationResult>;
|
|
29
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/server/validator/validate.ts"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAsBD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA8BjF"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { getNginxBinary } from './nginx-binary.js';
|
|
6
|
+
/**
|
|
7
|
+
* Minimal `http {}` wrapper that lets NGINX type-check a renderer fragment.
|
|
8
|
+
*
|
|
9
|
+
* The renderer emits the *contents* of an `http {}` block (an `upstream`
|
|
10
|
+
* plus one or two `server` blocks). NGINX cannot validate that on its own —
|
|
11
|
+
* it needs `events {}` and an enclosing `http {}` to even parse. We wrap
|
|
12
|
+
* here rather than in the renderer so the renderer stays pure and we keep
|
|
13
|
+
* the validator's wrapping policy in one place.
|
|
14
|
+
*/
|
|
15
|
+
function buildWrapper(siteConfPath) {
|
|
16
|
+
return `events {
|
|
17
|
+
worker_connections 1024;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
http {
|
|
21
|
+
include ${siteConfPath};
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validates an NGINX config fragment with `nginx -t`.
|
|
27
|
+
*
|
|
28
|
+
* Strategy: write the fragment to a private temp dir, wrap it in a minimal
|
|
29
|
+
* `events {} http {}` shell, then invoke the NGINX binary on the wrapper.
|
|
30
|
+
* The wrapper uses an **absolute** include path — NGINX's `include`
|
|
31
|
+
* resolves relative paths against its own config-prefix, which we don't
|
|
32
|
+
* control.
|
|
33
|
+
*
|
|
34
|
+
* This function is side-effect-free from the caller's POV: it never mutates
|
|
35
|
+
* application state, never reloads NGINX, and always cleans its temp dir
|
|
36
|
+
* (cleanup errors are swallowed so they cannot mask the validation result).
|
|
37
|
+
* Phase 5 owns deciding what to do with a failed result.
|
|
38
|
+
*/
|
|
39
|
+
export async function validateConfig(candidate) {
|
|
40
|
+
const dir = await mkdtemp(join(tmpdir(), 'zoomies-validate-'));
|
|
41
|
+
try {
|
|
42
|
+
const sitePath = join(dir, 'site.conf');
|
|
43
|
+
const wrapperPath = join(dir, 'nginx.conf');
|
|
44
|
+
await writeFile(sitePath, candidate, 'utf8');
|
|
45
|
+
await writeFile(wrapperPath, buildWrapper(sitePath), 'utf8');
|
|
46
|
+
// `reject: false` so a non-zero exit code returns a result rather than
|
|
47
|
+
// throwing — failure is a normal, expected outcome of validation.
|
|
48
|
+
// Argv array (never a shell string) per the project's no-shell rule.
|
|
49
|
+
const result = await execa(getNginxBinary(), ['-t', '-c', wrapperPath], {
|
|
50
|
+
reject: false,
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
ok: result.exitCode === 0,
|
|
54
|
+
stdout: result.stdout ?? '',
|
|
55
|
+
stderr: result.stderr ?? '',
|
|
56
|
+
exitCode: result.exitCode ?? null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
// Cleanup must never mask the validation outcome — log and move on.
|
|
61
|
+
try {
|
|
62
|
+
await rm(dir, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.warn(`zoomies: failed to remove validator temp dir ${dir}:`, err);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../../src/server/validator/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAC9B,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAgBnD;;;;;;;;GAQG;AACH,SAAS,YAAY,CAAC,YAAoB;IACxC,OAAO;;;;;cAKK,YAAY;;CAEzB,CAAC;AACF,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAAiB;IACpD,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,mBAAmB,CAAC,CAAC,CAAC;IAC/D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QACxC,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QAE5C,MAAM,SAAS,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QAC7C,MAAM,SAAS,CAAC,WAAW,EAAE,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;QAE7D,uEAAuE;QACvE,kEAAkE;QAClE,qEAAqE;QACrE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,EAAE;YACtE,MAAM,EAAE,KAAK;SACd,CAAC,CAAC;QAEH,OAAO;YACL,EAAE,EAAE,MAAM,CAAC,QAAQ,KAAK,CAAC;YACzB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;YAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;YAC3B,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,IAAI;SAClC,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,oEAAoE;QACpE,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,gDAAgD,GAAG,GAAG,EAAE,GAAG,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Long-running renewal worker entrypoint.
|
|
4
|
+
*
|
|
5
|
+
* `main()` runs an outer loop that calls {@link runRenewalCheck} every
|
|
6
|
+
* {@link WorkerOptions.intervalMs} (default 6 hours), resolves the production
|
|
7
|
+
* dependencies from the environment once on startup, and races the wait
|
|
8
|
+
* between iterations against a SIGTERM/SIGINT-driven abort promise so the
|
|
9
|
+
* worker shuts down promptly on signal.
|
|
10
|
+
*
|
|
11
|
+
* Shutdown semantics: we never interrupt a renewal mid-flight. The signal
|
|
12
|
+
* handlers flip a flag and resolve the abort promise — the loop reads it
|
|
13
|
+
* **after** the current `runRenewalCheck` returns, then exits cleanly. Let's
|
|
14
|
+
* Encrypt orders are stateful and a half-completed order tends to consume
|
|
15
|
+
* rate limit without producing a usable cert, so the cost of a clean wait
|
|
16
|
+
* is worth more than a few seconds of shutdown latency.
|
|
17
|
+
*
|
|
18
|
+
* Importable + runnable: the bottom of this file uses the same
|
|
19
|
+
* `import.meta.url === pathToFileURL(process.argv[1]).href` shim as
|
|
20
|
+
* `src/index.ts`, so it works both as `node dist/server/worker/main.js` and
|
|
21
|
+
* as an imported module from tests.
|
|
22
|
+
*
|
|
23
|
+
* The leading `#!/usr/bin/env node` shebang must stay on line 1 for the
|
|
24
|
+
* `zoomies-worker` bin entry to be directly executable; TypeScript happily
|
|
25
|
+
* carries the line through to the emitted JS as a comment-shaped no-op.
|
|
26
|
+
*/
|
|
27
|
+
export interface WorkerOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Polling cadence between renewal checks. Defaults to 6 hours — short
|
|
30
|
+
* enough that a missed window heals on its own within a day, long enough
|
|
31
|
+
* to keep journal noise low.
|
|
32
|
+
*/
|
|
33
|
+
intervalMs?: number;
|
|
34
|
+
/** If true, run one renewal check and return. Used by CI smoke tests. */
|
|
35
|
+
once?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Worker entrypoint. Resolves the production deps from the environment,
|
|
39
|
+
* then loops `runRenewalCheck` on the configured cadence until a shutdown
|
|
40
|
+
* signal is received (or `opts.once` is true).
|
|
41
|
+
*/
|
|
42
|
+
export declare function main(opts?: WorkerOptions): Promise<void>;
|
|
43
|
+
//# sourceMappingURL=main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../src/server/worker/main.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAcH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAwGD;;;;GAIG;AACH,wBAAsB,IAAI,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA0D9D"}
|