@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 CHANGED
@@ -2,7 +2,6 @@
2
2
  .gitignore
3
3
  .project.json
4
4
  node_modules
5
- dist
6
5
  coverage
7
6
  *.tgz
8
7
  *.log
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 the `hasna-xyz-infra` target,
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 uptime && adduser --system --ingroup uptime uptime
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,CA2BrJ"}
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 cloud database adapter is not implemented yet");
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
- if (this.mode === "hosted" && !allowHostedLocalStore(options.allowHostedLocalStore)) {
866
- throw new Error("hosted mode requires a cloud data layer; set HASNA_UPTIME_ALLOW_HOSTED_LOCAL_STORE=1 only for explicit local fallback testing");
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
- this.dataMode = this.mode === "hosted" ? "hosted-local-sqlite" : "local-sqlite";
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
- const origin = request.headers.get("origin");
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;