@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,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Periodic renewal scheduler for ACME-issued certificates.
|
|
3
|
+
*
|
|
4
|
+
* The scheduler walks the cert repository, filters to `provider === 'acme'`
|
|
5
|
+
* certificates whose `notAfter` falls within a configurable lead window, and
|
|
6
|
+
* delegates each to a pre-bound {@link RenewalSchedulerDeps.renew} closure.
|
|
7
|
+
* Renewals are awaited **sequentially** — Let's Encrypt rate limits favour
|
|
8
|
+
* one-order-at-a-time over parallel bursts, and a serial loop also keeps
|
|
9
|
+
* accounting simple if any one renewal fails partway through.
|
|
10
|
+
*
|
|
11
|
+
* This module is intentionally pure-ish: no `setTimeout`, no infinite loop,
|
|
12
|
+
* no signal handling. The {@link main} worker in `src/server/worker/main.ts`
|
|
13
|
+
* drives the cadence and owns the process lifecycle.
|
|
14
|
+
*
|
|
15
|
+
* Failure handling: a thrown renewal is logged, recorded in
|
|
16
|
+
* {@link RenewalRunResult.failed}, and the loop continues with the next cert.
|
|
17
|
+
* A single bad cert (e.g. a dropped DNS record) must not block renewals for
|
|
18
|
+
* every other domain on the host.
|
|
19
|
+
*/
|
|
20
|
+
import type { Cert } from '../domain/cert.js';
|
|
21
|
+
import type { CertRepository } from '../repositories/cert-repository.js';
|
|
22
|
+
export interface RenewalSchedulerDeps {
|
|
23
|
+
certRepo: CertRepository;
|
|
24
|
+
/**
|
|
25
|
+
* Per-cert renewal closure. Production code pre-binds the ACME account,
|
|
26
|
+
* challenge store, cert directory, and repository so the scheduler only
|
|
27
|
+
* needs to pass the cert row.
|
|
28
|
+
*/
|
|
29
|
+
renew: (cert: Cert) => Promise<Cert>;
|
|
30
|
+
/** Time source. Defaults to `() => new Date()`; injected for determinism. */
|
|
31
|
+
now?: () => Date;
|
|
32
|
+
/**
|
|
33
|
+
* Structured-ish logger. Defaults to `console.info`. The first arg is a
|
|
34
|
+
* human-readable message; the optional second arg is a metadata bag so
|
|
35
|
+
* production logs can be parsed by a journal collector without regex.
|
|
36
|
+
*/
|
|
37
|
+
log?: (message: string, meta?: Record<string, unknown>) => void;
|
|
38
|
+
}
|
|
39
|
+
export interface RenewalSchedulerOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Renew certificates whose `notAfter` lies within this many days of `now`.
|
|
42
|
+
* Defaults to 30, matching Let's Encrypt's recommended renewal threshold
|
|
43
|
+
* (90-day lifetime, renew at T-30).
|
|
44
|
+
*/
|
|
45
|
+
renewWithinDays?: number;
|
|
46
|
+
deps: RenewalSchedulerDeps;
|
|
47
|
+
}
|
|
48
|
+
export interface RenewalRunResult {
|
|
49
|
+
/** Number of `acme` certs that were inspected for renewal eligibility. */
|
|
50
|
+
checked: number;
|
|
51
|
+
/** Number of certs successfully renewed in this run. */
|
|
52
|
+
renewed: number;
|
|
53
|
+
/** One entry per failed renewal, in iteration order. */
|
|
54
|
+
failed: Array<{
|
|
55
|
+
domain: string;
|
|
56
|
+
error: string;
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Execute a single renewal pass.
|
|
61
|
+
*
|
|
62
|
+
* Lists every cert, filters to ACME-managed rows whose validity window ends
|
|
63
|
+
* within the configured lead time, and awaits each renewal in turn. Returns
|
|
64
|
+
* a summary describing what happened — the worker logs it and decides when
|
|
65
|
+
* to run again.
|
|
66
|
+
*/
|
|
67
|
+
export declare function runRenewalCheck(opts: RenewalSchedulerOptions): Promise<RenewalRunResult>;
|
|
68
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../../src/server/certs/scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB;;;;OAIG;IACH,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACjE;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,oBAAoB,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,MAAM,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAoBD;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmC9F"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Periodic renewal scheduler for ACME-issued certificates.
|
|
3
|
+
*
|
|
4
|
+
* The scheduler walks the cert repository, filters to `provider === 'acme'`
|
|
5
|
+
* certificates whose `notAfter` falls within a configurable lead window, and
|
|
6
|
+
* delegates each to a pre-bound {@link RenewalSchedulerDeps.renew} closure.
|
|
7
|
+
* Renewals are awaited **sequentially** — Let's Encrypt rate limits favour
|
|
8
|
+
* one-order-at-a-time over parallel bursts, and a serial loop also keeps
|
|
9
|
+
* accounting simple if any one renewal fails partway through.
|
|
10
|
+
*
|
|
11
|
+
* This module is intentionally pure-ish: no `setTimeout`, no infinite loop,
|
|
12
|
+
* no signal handling. The {@link main} worker in `src/server/worker/main.ts`
|
|
13
|
+
* drives the cadence and owns the process lifecycle.
|
|
14
|
+
*
|
|
15
|
+
* Failure handling: a thrown renewal is logged, recorded in
|
|
16
|
+
* {@link RenewalRunResult.failed}, and the loop continues with the next cert.
|
|
17
|
+
* A single bad cert (e.g. a dropped DNS record) must not block renewals for
|
|
18
|
+
* every other domain on the host.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_RENEW_WITHIN_DAYS = 30;
|
|
21
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
22
|
+
function defaultLog(message, meta) {
|
|
23
|
+
if (meta === undefined) {
|
|
24
|
+
console.info(message);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.info(message, meta);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function formatError(err) {
|
|
31
|
+
if (err instanceof Error) {
|
|
32
|
+
return err.message;
|
|
33
|
+
}
|
|
34
|
+
return String(err);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Execute a single renewal pass.
|
|
38
|
+
*
|
|
39
|
+
* Lists every cert, filters to ACME-managed rows whose validity window ends
|
|
40
|
+
* within the configured lead time, and awaits each renewal in turn. Returns
|
|
41
|
+
* a summary describing what happened — the worker logs it and decides when
|
|
42
|
+
* to run again.
|
|
43
|
+
*/
|
|
44
|
+
export async function runRenewalCheck(opts) {
|
|
45
|
+
const renewWithinDays = opts.renewWithinDays ?? DEFAULT_RENEW_WITHIN_DAYS;
|
|
46
|
+
const { certRepo, renew } = opts.deps;
|
|
47
|
+
const now = opts.deps.now ?? (() => new Date());
|
|
48
|
+
const log = opts.deps.log ?? defaultLog;
|
|
49
|
+
const threshold = new Date(now().getTime() + renewWithinDays * MS_PER_DAY);
|
|
50
|
+
// Filter to ACME-managed certs whose validity window ends before the
|
|
51
|
+
// threshold. Manual certs are owned by the operator — the scheduler
|
|
52
|
+
// never touches them.
|
|
53
|
+
const candidates = certRepo
|
|
54
|
+
.list()
|
|
55
|
+
.filter((cert) => cert.provider === 'acme')
|
|
56
|
+
.filter((cert) => Date.parse(cert.notAfter) <= threshold.getTime());
|
|
57
|
+
const result = {
|
|
58
|
+
checked: candidates.length,
|
|
59
|
+
renewed: 0,
|
|
60
|
+
failed: [],
|
|
61
|
+
};
|
|
62
|
+
for (const cert of candidates) {
|
|
63
|
+
try {
|
|
64
|
+
await renew(cert);
|
|
65
|
+
result.renewed += 1;
|
|
66
|
+
log('zoomies: renewed cert', { domain: cert.domain });
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
const message = formatError(err);
|
|
70
|
+
result.failed.push({ domain: cert.domain, error: message });
|
|
71
|
+
log('zoomies: renewal failed', { domain: cert.domain, error: message });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../../src/server/certs/scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AA0CH,MAAM,yBAAyB,GAAG,EAAE,CAAC;AACrC,MAAM,UAAU,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEvC,SAAS,UAAU,CAAC,OAAe,EAAE,IAA8B;IACjE,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,GAAY;IAC/B,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;QACzB,OAAO,GAAG,CAAC,OAAO,CAAC;IACrB,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;AACrB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAA6B;IACjE,MAAM,eAAe,GAAG,IAAI,CAAC,eAAe,IAAI,yBAAyB,CAAC;IAC1E,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC;IACtC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAChD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,UAAU,CAAC;IAExC,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,eAAe,GAAG,UAAU,CAAC,CAAC;IAE3E,qEAAqE;IACrE,oEAAoE;IACpE,sBAAsB;IACtB,MAAM,UAAU,GAAG,QAAQ;SACxB,IAAI,EAAE;SACN,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,MAAM,CAAC;SAC1C,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC;IAEtE,MAAM,MAAM,GAAqB;QAC/B,OAAO,EAAE,UAAU,CAAC,MAAM;QAC1B,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,EAAE;KACX,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;YAClB,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;YACpB,GAAG,CAAC,uBAAuB,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACxD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;YACjC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;YAC5D,GAAG,CAAC,yBAAyB,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
/**
|
|
3
|
+
* Open a SQLite database and apply the control-plane pragma defaults.
|
|
4
|
+
*
|
|
5
|
+
* - `journal_mode = WAL` — write-ahead logging for concurrent readers.
|
|
6
|
+
* - `foreign_keys = ON` — enforce FK constraints (off by default in SQLite).
|
|
7
|
+
* - `synchronous = NORMAL` — durable under WAL while staying fast.
|
|
8
|
+
*/
|
|
9
|
+
export declare function openDatabase(path: string): Database.Database;
|
|
10
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../src/server/db/connection.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAEtC;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAM5D"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
/**
|
|
3
|
+
* Open a SQLite database and apply the control-plane pragma defaults.
|
|
4
|
+
*
|
|
5
|
+
* - `journal_mode = WAL` — write-ahead logging for concurrent readers.
|
|
6
|
+
* - `foreign_keys = ON` — enforce FK constraints (off by default in SQLite).
|
|
7
|
+
* - `synchronous = NORMAL` — durable under WAL while staying fast.
|
|
8
|
+
*/
|
|
9
|
+
export function openDatabase(path) {
|
|
10
|
+
const db = new Database(path);
|
|
11
|
+
db.pragma('journal_mode = WAL');
|
|
12
|
+
db.pragma('foreign_keys = ON');
|
|
13
|
+
db.pragma('synchronous = NORMAL');
|
|
14
|
+
return db;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../../../src/server/db/connection.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAEtC;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC9B,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAChC,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAC/B,EAAE,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;IAClC,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
/**
|
|
3
|
+
* Apply pending SQL migrations from the sibling `migrations/` directory.
|
|
4
|
+
*
|
|
5
|
+
* Each `.sql` file is one migration; the version is the filename without
|
|
6
|
+
* its extension. Migrations are applied in filename-sorted order. Each
|
|
7
|
+
* application is wrapped in a transaction together with the
|
|
8
|
+
* `schema_migrations` bookkeeping insert, so a failure rolls back cleanly
|
|
9
|
+
* and a second call is a no-op.
|
|
10
|
+
*/
|
|
11
|
+
export declare function runMigrations(db: Database.Database): void;
|
|
12
|
+
//# sourceMappingURL=migrate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../src/server/db/migrate.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAU3C;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,IAAI,CA+BzD"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
const MIGRATIONS_DIR = path.join(__dirname, 'migrations');
|
|
7
|
+
/**
|
|
8
|
+
* Apply pending SQL migrations from the sibling `migrations/` directory.
|
|
9
|
+
*
|
|
10
|
+
* Each `.sql` file is one migration; the version is the filename without
|
|
11
|
+
* its extension. Migrations are applied in filename-sorted order. Each
|
|
12
|
+
* application is wrapped in a transaction together with the
|
|
13
|
+
* `schema_migrations` bookkeeping insert, so a failure rolls back cleanly
|
|
14
|
+
* and a second call is a no-op.
|
|
15
|
+
*/
|
|
16
|
+
export function runMigrations(db) {
|
|
17
|
+
db.exec('CREATE TABLE IF NOT EXISTS schema_migrations (version TEXT PRIMARY KEY, applied_at TEXT NOT NULL);');
|
|
18
|
+
const applied = new Set(db
|
|
19
|
+
.prepare('SELECT version FROM schema_migrations')
|
|
20
|
+
.all()
|
|
21
|
+
.map((row) => row.version));
|
|
22
|
+
const files = readdirSync(MIGRATIONS_DIR)
|
|
23
|
+
.filter((name) => name.endsWith('.sql'))
|
|
24
|
+
.sort();
|
|
25
|
+
const insertVersion = db.prepare('INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)');
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
const version = file.slice(0, -'.sql'.length);
|
|
28
|
+
if (applied.has(version))
|
|
29
|
+
continue;
|
|
30
|
+
const sql = readFileSync(path.join(MIGRATIONS_DIR, file), 'utf8');
|
|
31
|
+
db.transaction(() => {
|
|
32
|
+
db.exec(sql);
|
|
33
|
+
insertVersion.run(version, new Date().toISOString());
|
|
34
|
+
})();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../../src/server/db/migrate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAIzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAC3C,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AAM1D;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,EAAqB;IACjD,EAAE,CAAC,IAAI,CACL,oGAAoG,CACrG,CAAC;IAEF,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,EAAE;SACC,OAAO,CAAC,uCAAuC,CAAC;SAChD,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAE,GAAkB,CAAC,OAAO,CAAC,CAC7C,CAAC;IAEF,MAAM,KAAK,GAAG,WAAW,CAAC,cAAc,CAAC;SACtC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;SACvC,IAAI,EAAE,CAAC;IAEV,MAAM,aAAa,GAAG,EAAE,CAAC,OAAO,CAC9B,mEAAmE,CACpE,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC9C,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,SAAS;QAEnC,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;QAElE,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YAClB,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACb,aAAa,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;QACvD,CAAC,CAAC,EAAE,CAAC;IACP,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
CREATE TABLE upstreams (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
name TEXT NOT NULL,
|
|
4
|
+
load_balancer TEXT NOT NULL CHECK (load_balancer IN ('round_robin', 'least_conn', 'ip_hash')),
|
|
5
|
+
created_at TEXT NOT NULL,
|
|
6
|
+
updated_at TEXT NOT NULL
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
CREATE TABLE upstream_targets (
|
|
10
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
upstream_id TEXT NOT NULL REFERENCES upstreams(id) ON DELETE CASCADE,
|
|
12
|
+
host TEXT NOT NULL,
|
|
13
|
+
port INTEGER NOT NULL CHECK (port BETWEEN 1 AND 65535),
|
|
14
|
+
weight INTEGER NOT NULL CHECK (weight BETWEEN 1 AND 1000),
|
|
15
|
+
position INTEGER NOT NULL,
|
|
16
|
+
UNIQUE (upstream_id, position)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE TABLE sites (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
hostname TEXT NOT NULL UNIQUE,
|
|
22
|
+
upstream_id TEXT NOT NULL REFERENCES upstreams(id) ON DELETE RESTRICT,
|
|
23
|
+
tls_mode TEXT NOT NULL CHECK (tls_mode IN ('off', 'acme', 'manual')),
|
|
24
|
+
created_at TEXT NOT NULL,
|
|
25
|
+
updated_at TEXT NOT NULL
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE certs (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
domain TEXT NOT NULL UNIQUE,
|
|
31
|
+
provider TEXT NOT NULL CHECK (provider IN ('acme', 'manual')),
|
|
32
|
+
pem_path TEXT NOT NULL,
|
|
33
|
+
key_path TEXT NOT NULL,
|
|
34
|
+
not_before TEXT NOT NULL,
|
|
35
|
+
not_after TEXT NOT NULL,
|
|
36
|
+
created_at TEXT NOT NULL,
|
|
37
|
+
updated_at TEXT NOT NULL,
|
|
38
|
+
CHECK (not_before < not_after)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE INDEX idx_sites_upstream ON sites(upstream_id);
|
|
42
|
+
CREATE INDEX idx_targets_upstream ON upstream_targets(upstream_id);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const CertSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodUUID;
|
|
4
|
+
domain: z.ZodString;
|
|
5
|
+
provider: z.ZodEnum<{
|
|
6
|
+
acme: "acme";
|
|
7
|
+
manual: "manual";
|
|
8
|
+
}>;
|
|
9
|
+
pemPath: z.ZodString;
|
|
10
|
+
keyPath: z.ZodString;
|
|
11
|
+
notBefore: z.ZodISODateTime;
|
|
12
|
+
notAfter: z.ZodISODateTime;
|
|
13
|
+
createdAt: z.ZodISODateTime;
|
|
14
|
+
updatedAt: z.ZodISODateTime;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export type Cert = z.infer<typeof CertSchema>;
|
|
17
|
+
//# sourceMappingURL=cert.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cert.d.ts","sourceRoot":"","sources":["../../../src/server/domain/cert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;iBAenB,CAAC;AAEL,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// RFC 1123 hostname: labels of 1-63 chars (alphanumeric + hyphen, no leading
|
|
3
|
+
// or trailing hyphen), separated by dots. Defined locally so the cert module
|
|
4
|
+
// does not couple to the Site schema's hostname rules.
|
|
5
|
+
const DOMAIN_REGEX = /^(?=.{1,253}$)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
6
|
+
export const CertSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
id: z.uuid(),
|
|
9
|
+
domain: z.string().min(1).max(253).regex(DOMAIN_REGEX, 'must be a valid domain'),
|
|
10
|
+
provider: z.enum(['acme', 'manual']),
|
|
11
|
+
pemPath: z.string().min(1),
|
|
12
|
+
keyPath: z.string().min(1),
|
|
13
|
+
notBefore: z.iso.datetime(),
|
|
14
|
+
notAfter: z.iso.datetime(),
|
|
15
|
+
createdAt: z.iso.datetime(),
|
|
16
|
+
updatedAt: z.iso.datetime(),
|
|
17
|
+
})
|
|
18
|
+
.refine((cert) => Date.parse(cert.notBefore) < Date.parse(cert.notAfter), {
|
|
19
|
+
message: 'notBefore must be earlier than notAfter',
|
|
20
|
+
path: ['notAfter'],
|
|
21
|
+
});
|
|
22
|
+
//# sourceMappingURL=cert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cert.js","sourceRoot":"","sources":["../../../src/server/domain/cert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,6EAA6E;AAC7E,6EAA6E;AAC7E,uDAAuD;AACvD,MAAM,YAAY,GAChB,8GAA8G,CAAC;AAEjH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC;KACxB,MAAM,CAAC;IACN,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE;IACZ,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,YAAY,EAAE,wBAAwB,CAAC;IAChF,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACpC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1B,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;IAC3B,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;IAC1B,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;IAC3B,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;CAC5B,CAAC;KACD,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE;IACxE,OAAO,EAAE,yCAAyC;IAClD,IAAI,EAAE,CAAC,UAAU,CAAC;CACnB,CAAC,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain error hierarchy for the Zoomies control plane.
|
|
3
|
+
*
|
|
4
|
+
* These errors model failure modes inside the pure domain layer:
|
|
5
|
+
* a record is missing, a uniqueness/state constraint is violated, or
|
|
6
|
+
* input could not be reconciled with a domain invariant. They carry a
|
|
7
|
+
* stable `code` discriminator so transport layers (Route Handlers, CLI)
|
|
8
|
+
* can map them to HTTP statuses / exit codes without `instanceof` chains.
|
|
9
|
+
*
|
|
10
|
+
* No I/O. No Zod. No entity coupling.
|
|
11
|
+
*/
|
|
12
|
+
export declare abstract class DomainError extends Error {
|
|
13
|
+
abstract readonly code: string;
|
|
14
|
+
constructor(message: string, options?: {
|
|
15
|
+
cause?: unknown;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export declare class NotFoundError extends DomainError {
|
|
19
|
+
readonly code = "not_found";
|
|
20
|
+
constructor(message: string, options?: {
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export declare class ConflictError extends DomainError {
|
|
25
|
+
readonly code = "conflict";
|
|
26
|
+
constructor(message: string, options?: {
|
|
27
|
+
cause?: unknown;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export declare class ValidationError extends DomainError {
|
|
31
|
+
readonly code = "validation";
|
|
32
|
+
constructor(message: string, options?: {
|
|
33
|
+
cause?: unknown;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/server/domain/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,8BAAsB,WAAY,SAAQ,KAAK;IAC7C,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAK3D;AAED,qBAAa,aAAc,SAAQ,WAAW;IAC5C,QAAQ,CAAC,IAAI,eAAe;gBAEhB,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAG3D;AAED,qBAAa,aAAc,SAAQ,WAAW;IAC5C,QAAQ,CAAC,IAAI,cAAc;gBAEf,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAG3D;AAED,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,QAAQ,CAAC,IAAI,gBAAgB;gBAEjB,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAG3D"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain error hierarchy for the Zoomies control plane.
|
|
3
|
+
*
|
|
4
|
+
* These errors model failure modes inside the pure domain layer:
|
|
5
|
+
* a record is missing, a uniqueness/state constraint is violated, or
|
|
6
|
+
* input could not be reconciled with a domain invariant. They carry a
|
|
7
|
+
* stable `code` discriminator so transport layers (Route Handlers, CLI)
|
|
8
|
+
* can map them to HTTP statuses / exit codes without `instanceof` chains.
|
|
9
|
+
*
|
|
10
|
+
* No I/O. No Zod. No entity coupling.
|
|
11
|
+
*/
|
|
12
|
+
export class DomainError extends Error {
|
|
13
|
+
constructor(message, options) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
// Ensure stack traces show the concrete subclass name, not "Error".
|
|
16
|
+
this.name = new.target.name;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class NotFoundError extends DomainError {
|
|
20
|
+
code = 'not_found';
|
|
21
|
+
constructor(message, options) {
|
|
22
|
+
super(message, options);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class ConflictError extends DomainError {
|
|
26
|
+
code = 'conflict';
|
|
27
|
+
constructor(message, options) {
|
|
28
|
+
super(message, options);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class ValidationError extends DomainError {
|
|
32
|
+
code = 'validation';
|
|
33
|
+
constructor(message, options) {
|
|
34
|
+
super(message, options);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/server/domain/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,OAAgB,WAAY,SAAQ,KAAK;IAG7C,YAAY,OAAe,EAAE,OAA6B;QACxD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,oEAAoE;QACpE,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AAED,MAAM,OAAO,aAAc,SAAQ,WAAW;IACnC,IAAI,GAAG,WAAW,CAAC;IAE5B,YAAY,OAAe,EAAE,OAA6B;QACxD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1B,CAAC;CACF;AAED,MAAM,OAAO,aAAc,SAAQ,WAAW;IACnC,IAAI,GAAG,UAAU,CAAC;IAE3B,YAAY,OAAe,EAAE,OAA6B;QACxD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1B,CAAC;CACF;AAED,MAAM,OAAO,eAAgB,SAAQ,WAAW;IACrC,IAAI,GAAG,YAAY,CAAC;IAE7B,YAAY,OAAe,EAAE,OAA6B;QACxD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1B,CAAC;CACF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const SiteSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
hostname: z.ZodString;
|
|
5
|
+
upstreamId: z.ZodString;
|
|
6
|
+
tlsMode: z.ZodEnum<{
|
|
7
|
+
off: "off";
|
|
8
|
+
acme: "acme";
|
|
9
|
+
manual: "manual";
|
|
10
|
+
}>;
|
|
11
|
+
createdAt: z.ZodString;
|
|
12
|
+
updatedAt: z.ZodString;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
export type Site = z.infer<typeof SiteSchema>;
|
|
15
|
+
//# sourceMappingURL=site.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site.d.ts","sourceRoot":"","sources":["../../../src/server/domain/site.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAexB,eAAO,MAAM,UAAU;;;;;;;;;;;iBAWrB,CAAC;AAEH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Hostname regex (RFC 1123-flavoured, restricted to lowercase).
|
|
4
|
+
*
|
|
5
|
+
* - Each label is 1–63 characters long, drawn from [a-z0-9-].
|
|
6
|
+
* - Labels may NOT start or end with a hyphen.
|
|
7
|
+
* - Labels are separated by a single dot. A trailing dot is rejected.
|
|
8
|
+
* - A single-label hostname (e.g. `localhost`) is permitted.
|
|
9
|
+
*
|
|
10
|
+
* The overall length (max 253 chars) is enforced separately via `.max(253)`.
|
|
11
|
+
*/
|
|
12
|
+
const HOSTNAME_REGEX = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/;
|
|
13
|
+
export const SiteSchema = z.object({
|
|
14
|
+
id: z.string().uuid(),
|
|
15
|
+
hostname: z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1)
|
|
18
|
+
.max(253)
|
|
19
|
+
.regex(HOSTNAME_REGEX, 'must be a lowercase RFC 1123 hostname'),
|
|
20
|
+
upstreamId: z.string().uuid(),
|
|
21
|
+
tlsMode: z.enum(['off', 'acme', 'manual']),
|
|
22
|
+
createdAt: z.string().datetime(),
|
|
23
|
+
updatedAt: z.string().datetime(),
|
|
24
|
+
});
|
|
25
|
+
//# sourceMappingURL=site.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site.js","sourceRoot":"","sources":["../../../src/server/domain/site.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;GASG;AACH,MAAM,cAAc,GAClB,mFAAmF,CAAC;AAEtF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC;IACjC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IACrB,QAAQ,EAAE,CAAC;SACR,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,GAAG,CAAC;SACR,KAAK,CAAC,cAAc,EAAE,uCAAuC,CAAC;IACjE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IAC7B,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC1C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const UpstreamTargetSchema: z.ZodObject<{
|
|
3
|
+
host: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6, z.ZodString]>;
|
|
4
|
+
port: z.ZodNumber;
|
|
5
|
+
weight: z.ZodNumber;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const UpstreamSchema: z.ZodObject<{
|
|
8
|
+
id: z.ZodUUID;
|
|
9
|
+
name: z.ZodString;
|
|
10
|
+
targets: z.ZodArray<z.ZodObject<{
|
|
11
|
+
host: z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6, z.ZodString]>;
|
|
12
|
+
port: z.ZodNumber;
|
|
13
|
+
weight: z.ZodNumber;
|
|
14
|
+
}, z.core.$strip>>;
|
|
15
|
+
loadBalancer: z.ZodEnum<{
|
|
16
|
+
round_robin: "round_robin";
|
|
17
|
+
least_conn: "least_conn";
|
|
18
|
+
ip_hash: "ip_hash";
|
|
19
|
+
}>;
|
|
20
|
+
createdAt: z.ZodISODateTime;
|
|
21
|
+
updatedAt: z.ZodISODateTime;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
export type UpstreamTarget = z.infer<typeof UpstreamTargetSchema>;
|
|
24
|
+
export type Upstream = z.infer<typeof UpstreamSchema>;
|
|
25
|
+
//# sourceMappingURL=upstream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upstream.d.ts","sourceRoot":"","sources":["../../../src/server/domain/upstream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,eAAO,MAAM,oBAAoB;;;;iBAI/B,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;iBAOzB,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// RFC 1123 hostname: labels of 1-63 chars (alphanumeric + hyphen, no leading
|
|
3
|
+
// or trailing hyphen), separated by dots. Total length capped at 253 chars
|
|
4
|
+
// by the outer z.string().max() below.
|
|
5
|
+
const HOSTNAME_REGEX = /^(?=.{1,253}$)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
6
|
+
const HostnameOrIpSchema = z.union([
|
|
7
|
+
z.ipv4(),
|
|
8
|
+
z.ipv6(),
|
|
9
|
+
z.string().min(1).max(253).regex(HOSTNAME_REGEX, 'must be a valid hostname'),
|
|
10
|
+
]);
|
|
11
|
+
export const UpstreamTargetSchema = z.object({
|
|
12
|
+
host: HostnameOrIpSchema,
|
|
13
|
+
port: z.number().int().min(1).max(65535),
|
|
14
|
+
weight: z.number().int().min(1).max(1000),
|
|
15
|
+
});
|
|
16
|
+
export const UpstreamSchema = z.object({
|
|
17
|
+
id: z.uuid(),
|
|
18
|
+
name: z.string().min(1).max(100),
|
|
19
|
+
targets: z.array(UpstreamTargetSchema).min(1).max(64),
|
|
20
|
+
loadBalancer: z.enum(['round_robin', 'least_conn', 'ip_hash']),
|
|
21
|
+
createdAt: z.iso.datetime(),
|
|
22
|
+
updatedAt: z.iso.datetime(),
|
|
23
|
+
});
|
|
24
|
+
//# sourceMappingURL=upstream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upstream.js","sourceRoot":"","sources":["../../../src/server/domain/upstream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,6EAA6E;AAC7E,2EAA2E;AAC3E,uCAAuC;AACvC,MAAM,cAAc,GAClB,8GAA8G,CAAC;AAEjH,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC;IACjC,CAAC,CAAC,IAAI,EAAE;IACR,CAAC,CAAC,IAAI,EAAE;IACR,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,0BAA0B,CAAC;CAC7E,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,IAAI,EAAE,kBAAkB;IACxB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;IACxC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;CAC1C,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE;IACZ,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAChC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;IACrD,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC;IAC9D,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;IAC3B,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;CAC5B,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,kEAAkE;AAClE,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rollback handle returned from {@link writeAtomic} and {@link deleteAtomic}.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator (Phase 5 Stage 2) collects these handles in order so it
|
|
5
|
+
* can unwind a multi-file change when `nginx -t` rejects the new bundle.
|
|
6
|
+
* `restore()` must be idempotent: calling it twice is a no-op the second
|
|
7
|
+
* time (and any time after).
|
|
8
|
+
*/
|
|
9
|
+
export interface AtomicRollback {
|
|
10
|
+
restore(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Atomically write `contents` to `path`, returning a rollback handle that
|
|
14
|
+
* can restore the prior state.
|
|
15
|
+
*
|
|
16
|
+
* Algorithm:
|
|
17
|
+
* 1. Snapshot the prior state of `path` (contents + mode, or "didn't
|
|
18
|
+
* exist") into memory.
|
|
19
|
+
* 2. Write `contents` to `${path}.new` in the same directory.
|
|
20
|
+
* 3. fsync the `.new` file so its data is durable before rename.
|
|
21
|
+
* 4. `rename(${path}.new, path)` — atomic on POSIX same-filesystem.
|
|
22
|
+
* 5. fsync the parent directory so the rename itself is durable.
|
|
23
|
+
*
|
|
24
|
+
* Rollback (`restore()`):
|
|
25
|
+
* - If the prior state was "didn't exist", `unlink(path)`.
|
|
26
|
+
* - Otherwise run the same atomic-replace dance with the stashed bytes
|
|
27
|
+
* and mode.
|
|
28
|
+
* - Idempotent: a second `restore()` call is a no-op.
|
|
29
|
+
*/
|
|
30
|
+
export declare function writeAtomic(path: string, contents: string): Promise<AtomicRollback>;
|
|
31
|
+
/**
|
|
32
|
+
* Atomically delete `path`, returning a rollback handle that restores the
|
|
33
|
+
* file byte-exact (including its mode).
|
|
34
|
+
*
|
|
35
|
+
* Throws {@link NotFoundError} if the target path does not exist — unlike
|
|
36
|
+
* `writeAtomic`, delete is meaningless against an absent file and the
|
|
37
|
+
* orchestrator should treat that as a precondition failure.
|
|
38
|
+
*
|
|
39
|
+
* Rollback (`restore()`):
|
|
40
|
+
* - Atomically re-creates the file with the stashed contents and mode.
|
|
41
|
+
* - Idempotent: a second `restore()` is a no-op.
|
|
42
|
+
*/
|
|
43
|
+
export declare function deleteAtomic(path: string): Promise<AtomicRollback>;
|
|
44
|
+
//# sourceMappingURL=atomic-write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"atomic-write.d.ts","sourceRoot":"","sources":["../../../src/server/reload/atomic-write.ts"],"names":[],"mappings":"AAKA;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AA0FD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CA6BzF;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAoBxE"}
|