@hasna/uptime 0.1.6 → 0.1.8
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/.dockerignore +0 -1
- package/CHANGELOG.md +35 -1
- package/Dockerfile +2 -1
- package/Dockerfile.package +22 -0
- package/README.md +13 -1
- package/dist/api.d.ts +2 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +81 -12
- package/dist/cli/index.js +148 -51
- package/dist/cloud-plan.d.ts +15 -4
- package/dist/cloud-plan.d.ts.map +1 -1
- package/dist/cloud-plan.js +55 -35
- package/dist/index.js +136 -47
- package/dist/mcp/index.js +38 -8
- package/dist/service.d.ts +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +38 -8
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +40 -9
- package/docs/aws-deployment-runbook.md +44 -22
- package/infra/aws/README.md +27 -6
- package/infra/aws/main.tf +374 -36
- package/infra/aws/outputs.tf +20 -0
- package/infra/aws/terraform.tfvars.example +13 -12
- package/infra/aws/variables.tf +48 -22
- package/package.json +2 -1
package/.dockerignore
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.8] - 2026-06-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- CloudFront default-domain protected web access mode for first AWS deployment,
|
|
14
|
+
with ALB HTTP restricted to CloudFront origin-facing ranges.
|
|
15
|
+
- Hosted public-origin allow-list support through
|
|
16
|
+
`HASNA_UPTIME_ALLOWED_ORIGINS`, wired by the AWS template for CloudFront and
|
|
17
|
+
custom HTTPS access modes.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- AWS Terraform and cloud-plan defaults no longer require custom Route53/ACM
|
|
22
|
+
inputs for the first protected web deployment path.
|
|
23
|
+
|
|
24
|
+
## [0.1.7] - 2026-06-28
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Explicit hosted EFS-backed SQLite runtime path with
|
|
29
|
+
`HASNA_UPTIME_HOSTED_SQLITE_DB` and `hosted-efs-sqlite` health metadata.
|
|
30
|
+
- AWS Terraform EFS file system, access point, ECS volume mount, and AWS Backup
|
|
31
|
+
plan for the hosted SQLite data store.
|
|
32
|
+
- `Dockerfile.package` plus AWS CodeBuild image-builder Terraform resources to
|
|
33
|
+
build the published npm package into ECR without relying on local Docker.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Hosted AWS deployment artifacts no longer inject `HASNA_UPTIME_DATABASE_URL`;
|
|
38
|
+
the async Postgres adapter remains future work.
|
|
39
|
+
- The EFS-backed SQLite bridge is single-writer only: one web task maximum and
|
|
40
|
+
scheduler/public-probe/reporter services remain disabled until Postgres and
|
|
41
|
+
cloud leases exist.
|
|
42
|
+
|
|
9
43
|
## [0.1.6] - 2026-06-28
|
|
10
44
|
|
|
11
45
|
### Added
|
|
@@ -29,7 +63,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
29
63
|
|
|
30
64
|
### Added
|
|
31
65
|
|
|
32
|
-
- Dry-run AWS deployment plan generator for
|
|
66
|
+
- Dry-run AWS deployment plan generator for a reviewed AWS target,
|
|
33
67
|
covering ECS/Fargate services, ECR image commands, ALB/RDS/S3/Secrets/Logs
|
|
34
68
|
resources, rollback steps, and safety assertions.
|
|
35
69
|
- Spark01 hosted-targeted private probe preflight config generator with JSON and
|
package/Dockerfile
CHANGED
|
@@ -15,7 +15,8 @@ ENV NODE_ENV=production \
|
|
|
15
15
|
HASNA_UPTIME_MODE=hosted
|
|
16
16
|
WORKDIR /app
|
|
17
17
|
|
|
18
|
-
RUN addgroup --system
|
|
18
|
+
RUN addgroup --system --gid 10001 uptime \
|
|
19
|
+
&& adduser --system --uid 10001 --ingroup uptime uptime
|
|
19
20
|
|
|
20
21
|
COPY --from=build /app/package.json ./package.json
|
|
21
22
|
COPY --from=build /app/node_modules ./node_modules
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
FROM oven/bun:1.3.13-slim AS runtime
|
|
4
|
+
ENV NODE_ENV=production \
|
|
5
|
+
HASNA_UPTIME_MODE=hosted
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
|
|
8
|
+
RUN addgroup --system --gid 10001 uptime \
|
|
9
|
+
&& adduser --system --uid 10001 --ingroup uptime uptime
|
|
10
|
+
|
|
11
|
+
COPY package.json ./package.json
|
|
12
|
+
COPY dist ./dist
|
|
13
|
+
|
|
14
|
+
RUN bun install --production
|
|
15
|
+
|
|
16
|
+
USER uptime
|
|
17
|
+
EXPOSE 3899
|
|
18
|
+
|
|
19
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
20
|
+
CMD bun -e "const r = await fetch('http://127.0.0.1:3899/health'); process.exit(r.ok ? 0 : 1)"
|
|
21
|
+
|
|
22
|
+
CMD ["bun", "dist/cli/index.js", "serve", "--mode", "hosted", "--host", "0.0.0.0", "--port", "3899"]
|
package/README.md
CHANGED
|
@@ -48,7 +48,17 @@ in `docs/aws-deployment-runbook.md` is satisfied.
|
|
|
48
48
|
|
|
49
49
|
Deployment review artifacts live in `Dockerfile` and `infra/aws`. The Terraform
|
|
50
50
|
desired counts default to zero, and `uptime cloud plan --json` exposes the
|
|
51
|
-
format/init/validate/plan commands with `applyAllowed: false`.
|
|
51
|
+
format/init/validate/plan commands with `applyAllowed: false`. The first
|
|
52
|
+
protected access path uses the CloudFront default HTTPS domain with ALB origin
|
|
53
|
+
ingress restricted to CloudFront. The hosted web task must set
|
|
54
|
+
`HASNA_UPTIME_ALLOWED_ORIGINS` to the public HTTPS edge origin so same-origin
|
|
55
|
+
browser mutations still pass when the private origin hop is HTTP. Hosted AWS
|
|
56
|
+
runtime state currently uses explicit EFS-backed SQLite via
|
|
57
|
+
`HASNA_UPTIME_HOSTED_SQLITE_DB=/data/uptime/uptime.db` for one protected web
|
|
58
|
+
task maximum; do not set `HASNA_UPTIME_DATABASE_URL` until the async Postgres
|
|
59
|
+
adapter is implemented.
|
|
60
|
+
`Dockerfile.package` is used by the Terraform CodeBuild image builder to build
|
|
61
|
+
the published npm package into ECR from inside AWS.
|
|
52
62
|
|
|
53
63
|
Private/local probes can submit signed results from another machine:
|
|
54
64
|
|
|
@@ -81,6 +91,8 @@ State-changing API requests reject cross-origin browser requests and
|
|
|
81
91
|
non-loopback mutation hosts by default. For a trusted remote bind, set
|
|
82
92
|
`HASNA_UPTIME_API_TOKEN` or pass `uptime serve --api-token <token>` and send
|
|
83
93
|
`Authorization: Bearer <token>` or `X-Uptime-Token: <token>`.
|
|
94
|
+
Hosted mode additionally accepts comma-separated public origins from
|
|
95
|
+
`HASNA_UPTIME_ALLOWED_ORIGINS` for deployments behind a TLS-terminating edge.
|
|
84
96
|
Endpoints that accept request bodies require `content-type: application/json`.
|
|
85
97
|
|
|
86
98
|
## Uptime Semantics
|
package/dist/api.d.ts
CHANGED
|
@@ -9,12 +9,14 @@ export interface ServeOptions extends UptimeServiceOptions {
|
|
|
9
9
|
apiToken?: string;
|
|
10
10
|
hostedToken?: string;
|
|
11
11
|
hostedTokens?: HostedToken[];
|
|
12
|
+
hostedAllowedOrigins?: string[];
|
|
12
13
|
allowUnsafeRemoteMutations?: boolean;
|
|
13
14
|
}
|
|
14
15
|
export interface CreateApiHandlerOptions {
|
|
15
16
|
apiToken?: string;
|
|
16
17
|
hostedToken?: string;
|
|
17
18
|
hostedTokens?: HostedToken[];
|
|
19
|
+
hostedAllowedOrigins?: string[];
|
|
18
20
|
allowUnsafeRemoteMutations?: boolean;
|
|
19
21
|
fetchImpl?: typeof fetch;
|
|
20
22
|
trustedLoopback?: boolean;
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,YAAa,SAAQ,oBAAoB;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B;AAED,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,cAAc,GAAG,cAAc,GAAG,eAAe,GAAG,cAAc,CAAC;AAE7G,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,GAAE,uBAA4B,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA2BvI;AAED,wBAAgB,WAAW,CAAC,OAAO,GAAE,YAAiB,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,SAAS,CAAC,EAAE,eAAe,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,YAAa,SAAQ,oBAAoB;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B;AAED,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,cAAc,GAAG,cAAc,GAAG,eAAe,GAAG,cAAc,CAAC;AAE7G,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,GAAE,uBAA4B,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA2BvI;AAED,wBAAgB,WAAW,CAAC,OAAO,GAAE,YAAiB,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,SAAS,CAAC,EAAE,eAAe,CAAA;CAAE,CA4BrJ"}
|
package/dist/api.js
CHANGED
|
@@ -820,10 +820,12 @@ function ensureUptimeHome() {
|
|
|
820
820
|
}
|
|
821
821
|
|
|
822
822
|
// src/store.ts
|
|
823
|
-
import { copyFileSync, existsSync, mkdirSync as mkdirSync2, statSync } from "fs";
|
|
823
|
+
import { copyFileSync, existsSync, mkdirSync as mkdirSync2, statfsSync, statSync } from "fs";
|
|
824
824
|
import { dirname, join as join2 } from "path";
|
|
825
825
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
826
826
|
import { Database } from "bun:sqlite";
|
|
827
|
+
var DEFAULT_HOSTED_SQLITE_DB_PATH = "/data/uptime/uptime.db";
|
|
828
|
+
var NFS_SUPER_MAGIC = 26985;
|
|
827
829
|
var SECRET_URL_PARAM_PATTERN = /(token|secret|password|passwd|api[_-]?key|access[_-]?token|auth|credential|session)/i;
|
|
828
830
|
var REQUIRED_TABLES = [
|
|
829
831
|
"schema_migrations",
|
|
@@ -860,18 +862,39 @@ class UptimeStore {
|
|
|
860
862
|
this.mode = resolveRuntimeMode(options.mode ?? "local");
|
|
861
863
|
const cloudDatabaseUrl = options.cloudDatabaseUrl ?? process.env.HASNA_UPTIME_DATABASE_URL;
|
|
862
864
|
if (this.mode === "hosted" && cloudDatabaseUrl) {
|
|
863
|
-
throw new Error("hosted
|
|
865
|
+
throw new Error("hosted Postgres adapter is not implemented yet; use HASNA_UPTIME_HOSTED_SQLITE_DB on cloud-mounted storage for the current hosted deployment path");
|
|
864
866
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
+
const hostedSqliteDbPath = options.hostedSqliteDbPath ?? process.env.HASNA_UPTIME_HOSTED_SQLITE_DB;
|
|
868
|
+
if (this.mode === "hosted" && hostedSqliteDbPath) {
|
|
869
|
+
if (hostedSqliteDbPath === ":memory:" || !hostedSqliteDbPath.startsWith("/")) {
|
|
870
|
+
throw new Error("HASNA_UPTIME_HOSTED_SQLITE_DB must be an absolute path on mounted cloud storage");
|
|
871
|
+
}
|
|
872
|
+
const approvedHostedPath = hostedSqliteDbPath === DEFAULT_HOSTED_SQLITE_DB_PATH;
|
|
873
|
+
if (!approvedHostedPath && !allowHostedLocalStore(options.allowHostedLocalStore)) {
|
|
874
|
+
throw new Error(`HASNA_UPTIME_HOSTED_SQLITE_DB must be ${DEFAULT_HOSTED_SQLITE_DB_PATH}; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing`);
|
|
875
|
+
}
|
|
876
|
+
const verifiedCloudMount = approvedHostedPath && isNfsMount(dirname(hostedSqliteDbPath));
|
|
877
|
+
if (approvedHostedPath && !verifiedCloudMount && !allowHostedLocalStore(options.allowHostedLocalStore)) {
|
|
878
|
+
throw new Error(`${DEFAULT_HOSTED_SQLITE_DB_PATH} must be on a mounted EFS/NFS filesystem; refusing to create hosted task-local SQLite`);
|
|
879
|
+
}
|
|
880
|
+
this.dataMode = verifiedCloudMount ? "hosted-efs-sqlite" : "hosted-local-sqlite";
|
|
881
|
+
this.dbPath = hostedSqliteDbPath;
|
|
882
|
+
} else if (this.mode === "hosted") {
|
|
883
|
+
if (!allowHostedLocalStore(options.allowHostedLocalStore)) {
|
|
884
|
+
throw new Error("hosted mode requires HASNA_UPTIME_HOSTED_SQLITE_DB on mounted cloud storage; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing");
|
|
885
|
+
}
|
|
886
|
+
this.dataMode = "hosted-local-sqlite";
|
|
887
|
+
this.dbPath = options.dbPath ?? uptimeHostedFallbackDbPath();
|
|
888
|
+
} else {
|
|
889
|
+
this.dataMode = "local-sqlite";
|
|
890
|
+
this.dbPath = options.dbPath ?? uptimeDbPath();
|
|
867
891
|
}
|
|
868
|
-
|
|
869
|
-
this.dbPath = options.dbPath ?? (this.mode === "hosted" ? uptimeHostedFallbackDbPath() : uptimeDbPath());
|
|
870
|
-
if (this.dbPath !== ":memory:") {
|
|
892
|
+
if (this.dbPath !== ":memory:" && this.dataMode !== "hosted-efs-sqlite") {
|
|
871
893
|
mkdirSync2(dirname(this.dbPath), { recursive: true });
|
|
872
894
|
}
|
|
873
895
|
this.db = new Database(this.dbPath, { create: true });
|
|
874
|
-
this.db.run("PRAGMA journal_mode = WAL");
|
|
896
|
+
this.db.run(this.dataMode === "hosted-efs-sqlite" ? "PRAGMA journal_mode = DELETE" : "PRAGMA journal_mode = WAL");
|
|
897
|
+
this.db.run("PRAGMA busy_timeout = 5000");
|
|
875
898
|
this.db.run("PRAGMA foreign_keys = ON");
|
|
876
899
|
this.migrate();
|
|
877
900
|
}
|
|
@@ -1751,6 +1774,13 @@ function resolveRuntimeMode(mode) {
|
|
|
1751
1774
|
function allowHostedLocalStore(value) {
|
|
1752
1775
|
return value === true || process.env.HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE === "1";
|
|
1753
1776
|
}
|
|
1777
|
+
function isNfsMount(path) {
|
|
1778
|
+
try {
|
|
1779
|
+
return statfsSync(path).type === NFS_SUPER_MAGIC;
|
|
1780
|
+
} catch {
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1754
1784
|
function verifyBackupFile(backupPath) {
|
|
1755
1785
|
const db = new Database(backupPath, { readonly: true });
|
|
1756
1786
|
try {
|
|
@@ -3480,6 +3510,7 @@ function serveUptime(options = {}) {
|
|
|
3480
3510
|
apiToken: options.apiToken,
|
|
3481
3511
|
hostedToken: options.hostedToken,
|
|
3482
3512
|
hostedTokens: options.hostedTokens,
|
|
3513
|
+
hostedAllowedOrigins: options.hostedAllowedOrigins,
|
|
3483
3514
|
allowUnsafeRemoteMutations: options.allowUnsafeRemoteMutations,
|
|
3484
3515
|
trustedLoopback: isLoopbackHost(options.host ?? "127.0.0.1"),
|
|
3485
3516
|
mode
|
|
@@ -3539,13 +3570,23 @@ async function handleHostedRequest(service, request, url, options) {
|
|
|
3539
3570
|
const scope = hostedScopeFor(request.method, apiPath);
|
|
3540
3571
|
requireHostedActor(request, url, options, scope);
|
|
3541
3572
|
if (["POST", "PATCH", "DELETE"].includes(request.method)) {
|
|
3542
|
-
|
|
3543
|
-
if (origin && origin !== `${url.protocol}//${url.host}`) {
|
|
3544
|
-
throw new ApiError("cross-origin mutation rejected", 403);
|
|
3545
|
-
}
|
|
3573
|
+
validateHostedMutationOrigin(request, url, options);
|
|
3546
3574
|
}
|
|
3547
3575
|
return handleApiRoute(service, request, url, apiPath, options, true);
|
|
3548
3576
|
}
|
|
3577
|
+
function validateHostedMutationOrigin(request, url, options) {
|
|
3578
|
+
const rawOrigin = request.headers.get("origin");
|
|
3579
|
+
const origin = normalizeOrigin(rawOrigin);
|
|
3580
|
+
if (rawOrigin && !origin) {
|
|
3581
|
+
throw new ApiError("cross-origin mutation rejected", 403);
|
|
3582
|
+
}
|
|
3583
|
+
if (!origin)
|
|
3584
|
+
return;
|
|
3585
|
+
const allowedOrigins = new Set([`${url.protocol}//${url.host}`, ...resolveHostedAllowedOrigins(options)]);
|
|
3586
|
+
if (!allowedOrigins.has(origin)) {
|
|
3587
|
+
throw new ApiError("cross-origin mutation rejected", 403);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3549
3590
|
async function handleApiRoute(service, request, url, apiPath, options, hosted) {
|
|
3550
3591
|
if (request.method === "GET" && apiPath === "/api/summary") {
|
|
3551
3592
|
return json(service.summary());
|
|
@@ -3764,6 +3805,34 @@ function resolveHostedTokens(options) {
|
|
|
3764
3805
|
workspaceId: process.env.HASNA_UPTIME_WORKSPACE_ID ?? "default"
|
|
3765
3806
|
}];
|
|
3766
3807
|
}
|
|
3808
|
+
function resolveHostedAllowedOrigins(options) {
|
|
3809
|
+
const configured = options.hostedAllowedOrigins ?? splitCsv(process.env.HASNA_UPTIME_ALLOWED_ORIGINS);
|
|
3810
|
+
return configured.map((origin) => normalizeAllowedOrigin(origin)).filter((origin) => Boolean(origin));
|
|
3811
|
+
}
|
|
3812
|
+
function splitCsv(value) {
|
|
3813
|
+
if (!value)
|
|
3814
|
+
return [];
|
|
3815
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
3816
|
+
}
|
|
3817
|
+
function normalizeAllowedOrigin(value) {
|
|
3818
|
+
const origin = normalizeOrigin(value);
|
|
3819
|
+
if (!origin) {
|
|
3820
|
+
throw new ApiError(`invalid hosted allowed origin: ${value}`, 500);
|
|
3821
|
+
}
|
|
3822
|
+
return origin;
|
|
3823
|
+
}
|
|
3824
|
+
function normalizeOrigin(value) {
|
|
3825
|
+
if (!value?.trim())
|
|
3826
|
+
return;
|
|
3827
|
+
try {
|
|
3828
|
+
const parsed = new URL(value.trim());
|
|
3829
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
3830
|
+
return;
|
|
3831
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
3832
|
+
} catch {
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3767
3836
|
function safeTokenEqual(candidate, expected) {
|
|
3768
3837
|
if (!candidate)
|
|
3769
3838
|
return false;
|