anbaric-cloud-hosting 1.44.0 → 1.46.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anbaric-cloud-hosting",
3
- "version": "1.44.0",
3
+ "version": "1.46.0",
4
4
  "description": "Anbaric Cloud hosting service: Postgres-backed job persistence and queuing exposed over an HTTP API",
5
5
  "license": "MIT",
6
6
  "author": "chris@anbaric.ai",
@@ -18,18 +18,18 @@
18
18
  "@aws-sdk/client-s3": "^3.1110.0",
19
19
  "@aws-sdk/client-secrets-manager": "^3.700.0",
20
20
  "@aws-sdk/client-servicediscovery": "^3.1110.0",
21
- "anbaric-data-store": "^1.44.0",
22
- "anbaric-plugins": "^1.44.0",
23
- "anbaric-tsapi": "^1.44.0",
24
- "anbaric-web": "^1.44.0",
21
+ "anbaric-data-store": "^1.46.0",
22
+ "anbaric-plugins": "^1.46.0",
23
+ "anbaric-tsapi": "^1.46.0",
24
+ "anbaric-web": "^1.46.0",
25
25
  "esbuild": "^0.28.2",
26
26
  "pg": "^8.16.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^26.2.0",
30
30
  "@types/pg": "^8.15.0",
31
- "anbaric-impl-cloud": "^1.44.0",
32
- "anbaric-state-machine": "^1.44.0",
31
+ "anbaric-impl-cloud": "^1.46.0",
32
+ "anbaric-state-machine": "^1.46.0",
33
33
  "tsx": "^4.20.0",
34
34
  "typescript": "^7.0.2"
35
35
  },
@@ -2,39 +2,13 @@ import {Pool} from "pg";
2
2
  import {migrate} from "./Migrations";
3
3
  import {PLATFORM_MIGRATIONS} from "./PlatformMigrations";
4
4
 
5
- /* Brings the database up to date at boot, then settles the app's SQL role.
6
- The role is not a migration: its password comes from the environment and may
7
- be rotated, so it is re-applied every time rather than recorded as done. */
5
+ /* Brings the database up to date at boot. The platform owns its own database
6
+ and nothing else: app data lives in a separate cluster, and the role that
7
+ reaches it is created alongside that database rather than here, so the
8
+ platform never holds a credential that can make roles. */
8
9
  const ensureSchema = async (pool : Pool) : Promise<void> => {
9
10
  const applied = await migrate(pool, PLATFORM_MIGRATIONS);
10
11
  if (applied.length > 0) console.log(`Applied ${applied.length} migration(s): ${applied.join(", ")}`);
11
-
12
- await ensureAppRole(pool);
13
- };
14
-
15
- /* The shared SQL store for apps: a dedicated schema plus a login role scoped
16
- to it, so deployed apps reach only anbaric_app_data - never the platform's
17
- public (jobs/documents/queue) or anbaric_system schemas. */
18
- const ensureAppRole = async (pool : Pool) : Promise<void> => {
19
- const appSqlSchema = process.env.ANBARIC_SQL_SCHEMA ?? "anbaric_app_data";
20
- if (! /^[a-z_][a-z0-9_]*$/i.test(appSqlSchema)) throw new Error(`Invalid ANBARIC_SQL_SCHEMA "${appSqlSchema}"`);
21
- await pool.query(`CREATE SCHEMA IF NOT EXISTS ${appSqlSchema}`);
22
-
23
- const appDbPassword = process.env.ANBARIC_APP_DB_PASSWORD;
24
- if (! appDbPassword) return;
25
-
26
- await pool.query(`DO $$ BEGIN
27
- IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'anbaric_app') THEN CREATE ROLE anbaric_app LOGIN; END IF;
28
- END $$`);
29
- await pool.query(`ALTER ROLE anbaric_app LOGIN PASSWORD '${appDbPassword.replace(/'/g, "''")}'`);
30
- await pool.query(`ALTER ROLE anbaric_app SET search_path TO ${appSqlSchema}`);
31
- await pool.query(`GRANT USAGE, CREATE ON SCHEMA ${appSqlSchema} TO anbaric_app`);
32
- await pool.query(`GRANT ALL ON ALL TABLES IN SCHEMA ${appSqlSchema} TO anbaric_app`);
33
- await pool.query(`GRANT ALL ON ALL SEQUENCES IN SCHEMA ${appSqlSchema} TO anbaric_app`);
34
- await pool.query(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${appSqlSchema} GRANT ALL ON TABLES TO anbaric_app`);
35
- await pool.query(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${appSqlSchema} GRANT ALL ON SEQUENCES TO anbaric_app`);
36
- await pool.query("REVOKE ALL ON SCHEMA public FROM anbaric_app");
37
- await pool.query("REVOKE ALL ON SCHEMA anbaric_system FROM anbaric_app");
38
12
  };
39
13
 
40
14
  export { ensureSchema }
@@ -37,6 +37,7 @@ import {StateMachinesHandler} from "./handlers/StateMachinesHandler";
37
37
  import {PluginsHandler} from "./handlers/PluginsHandler";
38
38
  import {AuthenticationMiddleware} from "./middleware/AuthenticationMiddleware";
39
39
  import {SessionMiddleware} from "./middleware/SessionMiddleware";
40
+ import {TenantRoutingMiddleware} from "./middleware/TenantRoutingMiddleware";
40
41
  import {LoadedPlugin} from "../plugins/Plugin";
41
42
  import {Request} from "./Request";
42
43
  import {Router} from "./Router";
@@ -141,6 +142,7 @@ class HostingServer {
141
142
 
142
143
  this.publicServer = new Server(publicRouter, [
143
144
  new SessionMiddleware(),
145
+ new TenantRoutingMiddleware(tenant),
144
146
  new AuthenticationMiddleware(authenticator, tokenAuthenticator, openRequests, undefined, userDirectory, memberships),
145
147
  ]);
146
148
  this.internalServer = new Server(internalRouter);
@@ -0,0 +1,29 @@
1
+ import {Middleware} from "../Middleware";
2
+ import {Request} from "../Request";
3
+
4
+ /* Many tenants share one load balancer, which picks between them on the
5
+ x-anbaric-tenant header. If a request arrives here naming a different tenant,
6
+ something upstream is wrong - a listener rule with the wrong condition, a
7
+ duplicated priority, a slug reused before its old rule was removed. Nothing
8
+ is lost when that happens, because a session signed by another tenant fails
9
+ verification here anyway, but it fails silently and looks like being logged
10
+ out. Answering 421 turns it into a named, countable error instead.
11
+
12
+ A request that names no tenant passes: the header is optional, and an
13
+ unmanaged platform has no tenant of its own to compare against. */
14
+ class TenantRoutingMiddleware implements Middleware {
15
+
16
+ constructor(private tenant : string | undefined) {}
17
+
18
+ async apply(request : Request) : Promise<boolean> {
19
+ const claimed = String(request.header("x-anbaric-tenant") ?? "").trim();
20
+ if (! this.tenant || ! claimed || claimed === this.tenant) return true;
21
+
22
+ console.error(`[routing] a request for tenant "${claimed}" reached the platform for "${this.tenant}"`);
23
+ request.reply(421, { error: `This platform does not serve tenant "${claimed}"` });
24
+ return false;
25
+ }
26
+
27
+ }
28
+
29
+ export { TenantRoutingMiddleware }