anbaric-cloud-hosting 1.4.0 → 1.6.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 +5 -5
- package/src/hosting/HostingServer.ts +84 -97
- package/src/hosting/Middleware.ts +11 -0
- package/src/hosting/Request.ts +103 -0
- package/src/hosting/RequestHandler.ts +9 -0
- package/src/hosting/Router.ts +22 -319
- package/src/hosting/Server.ts +54 -0
- package/src/hosting/handlers/AppProxyHandler.ts +34 -0
- package/src/hosting/handlers/AppsHandler.ts +57 -0
- package/src/hosting/handlers/AuditsHandler.ts +50 -0
- package/src/hosting/handlers/ConsumersHandler.ts +24 -0
- package/src/hosting/handlers/DocumentsHandler.ts +44 -0
- package/src/hosting/handlers/JobsHandler.ts +60 -0
- package/src/hosting/handlers/PagesHandler.ts +22 -0
- package/src/hosting/handlers/PingHandler.ts +18 -0
- package/src/hosting/handlers/QueueHandler.ts +36 -0
- package/src/hosting/handlers/SecretsHandler.ts +42 -0
- package/src/hosting/handlers/StateMachinesHandler.ts +21 -0
- package/src/hosting/handlers/auth/AuthorizeCliHandler.ts +50 -0
- package/src/hosting/handlers/auth/KeysHandler.ts +36 -0
- package/src/hosting/handlers/auth/WhoamiHandler.ts +18 -0
- package/src/hosting/middleware/AuthenticationMiddleware.ts +52 -0
- package/src/hosting/middleware/SessionMiddleware.ts +21 -0
- package/src/index.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anbaric-cloud-hosting",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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",
|
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
"@aws-sdk/client-s3": "^3.1110.0",
|
|
18
18
|
"@aws-sdk/client-secrets-manager": "^3.700.0",
|
|
19
19
|
"@aws-sdk/client-servicediscovery": "^3.1110.0",
|
|
20
|
-
"anbaric-data-store": "^1.
|
|
21
|
-
"anbaric-tsapi": "^1.
|
|
20
|
+
"anbaric-data-store": "^1.6.0",
|
|
21
|
+
"anbaric-tsapi": "^1.6.0",
|
|
22
22
|
"pg": "^8.16.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^26.2.0",
|
|
26
26
|
"@types/pg": "^8.15.0",
|
|
27
|
-
"anbaric-cloud": "^1.
|
|
28
|
-
"anbaric-state-machine": "^1.
|
|
27
|
+
"anbaric-impl-cloud": "^1.6.0",
|
|
28
|
+
"anbaric-state-machine": "^1.6.0",
|
|
29
29
|
"tsx": "^4.20.0",
|
|
30
30
|
"typescript": "^7.0.2"
|
|
31
31
|
},
|
|
@@ -1,130 +1,117 @@
|
|
|
1
|
-
import {createServer, IncomingMessage, Server, ServerResponse} from "node:http";
|
|
2
|
-
import {AddressInfo} from "node:net";
|
|
3
1
|
import {JobPersistence, JsonStore, SecretStore} from "anbaric-tsapi";
|
|
4
2
|
import {BuildLayer} from "../app-management/BuildLayer";
|
|
5
3
|
import {AuditRecordStore} from "../auditing/AuditRecordStore";
|
|
6
|
-
import {Authenticator
|
|
4
|
+
import {Authenticator} from "../auth/Authenticator";
|
|
7
5
|
import {CliAuthorizer} from "../auth/CliAuthorizer";
|
|
8
|
-
import {Tenant} from "../auth/Tenant";
|
|
9
6
|
import {TokenAuthenticator} from "../auth/TokenAuthenticator";
|
|
10
|
-
import {User} from "../auth/User";
|
|
11
7
|
import {ConfirmableQueue} from "../queuing/ConfirmableQueue";
|
|
12
8
|
import {ConsumerRegistry} from "../queuing/ConsumerRegistry";
|
|
9
|
+
import {AppProxyHandler} from "./handlers/AppProxyHandler";
|
|
10
|
+
import {AppsHandler} from "./handlers/AppsHandler";
|
|
11
|
+
import {AuditsHandler} from "./handlers/AuditsHandler";
|
|
12
|
+
import {AuthorizeCliHandler} from "./handlers/auth/AuthorizeCliHandler";
|
|
13
|
+
import {KeysHandler} from "./handlers/auth/KeysHandler";
|
|
14
|
+
import {WhoamiHandler} from "./handlers/auth/WhoamiHandler";
|
|
15
|
+
import {ConsumersHandler} from "./handlers/ConsumersHandler";
|
|
16
|
+
import {DocumentsHandler} from "./handlers/DocumentsHandler";
|
|
17
|
+
import {JobsHandler} from "./handlers/JobsHandler";
|
|
18
|
+
import {PagesHandler} from "./handlers/PagesHandler";
|
|
19
|
+
import {PingHandler} from "./handlers/PingHandler";
|
|
20
|
+
import {QueueHandler} from "./handlers/QueueHandler";
|
|
21
|
+
import {SecretsHandler} from "./handlers/SecretsHandler";
|
|
22
|
+
import {StateMachinesHandler} from "./handlers/StateMachinesHandler";
|
|
23
|
+
import {AuthenticationMiddleware} from "./middleware/AuthenticationMiddleware";
|
|
24
|
+
import {SessionMiddleware} from "./middleware/SessionMiddleware";
|
|
25
|
+
import {Request} from "./Request";
|
|
13
26
|
import {Router} from "./Router";
|
|
27
|
+
import {Server} from "./Server";
|
|
14
28
|
|
|
15
|
-
const
|
|
29
|
+
const CLI_KEY_POLL = /^\/authorize-cli\/[^/]+\/poll$/;
|
|
16
30
|
|
|
31
|
+
/* The composition root: builds the handlers from the platform's
|
|
32
|
+
collaborators, registers them on the public and internal routers, and
|
|
33
|
+
runs one Server per entry point. The public server authenticates through
|
|
34
|
+
its middleware chain; the internal server exposes only the workflow
|
|
35
|
+
handlers, unauthenticated, and must never be publicly reachable. */
|
|
17
36
|
class HostingServer {
|
|
18
37
|
|
|
19
|
-
private
|
|
38
|
+
private publicServer : Server;
|
|
20
39
|
private internalServer : Server;
|
|
21
|
-
private router : Router;
|
|
22
40
|
|
|
23
41
|
constructor(persistence : JobPersistence, queue : ConfirmableQueue,
|
|
24
42
|
registry : ConsumerRegistry = new ConsumerRegistry(),
|
|
25
43
|
buildLayer? : BuildLayer,
|
|
26
44
|
documentStoreFor? : (collection : string) => JsonStore,
|
|
27
45
|
secretStore? : SecretStore,
|
|
28
|
-
|
|
46
|
+
authenticator? : Authenticator,
|
|
29
47
|
cliAuthorizer? : CliAuthorizer,
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
tokenAuthenticator? : TokenAuthenticator,
|
|
49
|
+
tenant? : string,
|
|
32
50
|
auditRecords? : AuditRecordStore) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const status = /^No .+ found/.test(message) ? 404 : 500;
|
|
60
|
-
this.reply(response, status, { error: message });
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private async handle(request : IncomingMessage, response : ServerResponse) : Promise<void> {
|
|
66
|
-
const path = request.url?.split("?")[0] ?? "/";
|
|
67
|
-
if (path === "/ping" && request.method === "GET") {
|
|
68
|
-
return this.reply(response, 200, this.tenant ? { status: "ok", tenant: this.tenant } : { status: "ok" });
|
|
51
|
+
const pages = new PagesHandler();
|
|
52
|
+
const ping = new PingHandler(tenant);
|
|
53
|
+
const jobs = new JobsHandler(persistence);
|
|
54
|
+
const queueHandler = new QueueHandler(queue);
|
|
55
|
+
const consumers = new ConsumersHandler(registry);
|
|
56
|
+
const stateMachines = new StateMachinesHandler(registry);
|
|
57
|
+
const documents = documentStoreFor && new DocumentsHandler(documentStoreFor);
|
|
58
|
+
const secrets = secretStore && new SecretsHandler(secretStore);
|
|
59
|
+
const audits = auditRecords && new AuditsHandler(auditRecords);
|
|
60
|
+
|
|
61
|
+
const publicRouter = new Router();
|
|
62
|
+
publicRouter.registerRoot(pages);
|
|
63
|
+
publicRouter.register("ping", ping);
|
|
64
|
+
publicRouter.register("audit", pages);
|
|
65
|
+
publicRouter.register("whoami", new WhoamiHandler());
|
|
66
|
+
publicRouter.register("jobs", jobs);
|
|
67
|
+
publicRouter.register("queue", queueHandler);
|
|
68
|
+
publicRouter.register("consumers", consumers);
|
|
69
|
+
publicRouter.register("state-machines", stateMachines);
|
|
70
|
+
if (documents) publicRouter.register("documents", documents);
|
|
71
|
+
if (secrets) publicRouter.register("secrets", secrets);
|
|
72
|
+
if (audits) publicRouter.register("audits", audits);
|
|
73
|
+
if (cliAuthorizer) {
|
|
74
|
+
publicRouter.register("authorize-cli", new AuthorizeCliHandler(cliAuthorizer, pages, tenant));
|
|
75
|
+
publicRouter.register("keys", new KeysHandler(cliAuthorizer));
|
|
76
|
+
publicRouter.register("manage-keys", pages);
|
|
69
77
|
}
|
|
70
|
-
if (
|
|
71
|
-
|
|
78
|
+
if (buildLayer) {
|
|
79
|
+
publicRouter.register("apps", new AppsHandler(buildLayer));
|
|
80
|
+
publicRouter.registerFallback(new AppProxyHandler(buildLayer));
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (!resource || !INTERNAL_RESOURCES.has(resource)) {
|
|
94
|
-
return this.reply(response, 404, { error: "Not found" });
|
|
95
|
-
}
|
|
96
|
-
await this.router.route(request, response);
|
|
83
|
+
const internalRouter = new Router();
|
|
84
|
+
internalRouter.register("ping", ping);
|
|
85
|
+
internalRouter.register("jobs", jobs);
|
|
86
|
+
internalRouter.register("queue", queueHandler);
|
|
87
|
+
internalRouter.register("consumers", consumers);
|
|
88
|
+
internalRouter.register("state-machines", stateMachines);
|
|
89
|
+
if (documents) internalRouter.register("documents", documents);
|
|
90
|
+
if (secrets) internalRouter.register("secrets", secrets);
|
|
91
|
+
if (audits) internalRouter.register("audits", audits);
|
|
92
|
+
|
|
93
|
+
const openRequests = (request : Request) =>
|
|
94
|
+
request.url.pathname === "/ping" ||
|
|
95
|
+
(request.method === "GET" && CLI_KEY_POLL.test(request.url.pathname));
|
|
96
|
+
|
|
97
|
+
this.publicServer = new Server(publicRouter, [
|
|
98
|
+
new SessionMiddleware(),
|
|
99
|
+
new AuthenticationMiddleware(authenticator, tokenAuthenticator, openRequests),
|
|
100
|
+
]);
|
|
101
|
+
this.internalServer = new Server(internalRouter);
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (this.authenticator) {
|
|
102
|
-
const permitted = await this.authenticator.authorize(user, request, response);
|
|
103
|
-
if (!permitted) {
|
|
104
|
-
if (!response.writableEnded) this.reply(response, 403, { error: "Not authorized" });
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
await this.router.route(request, response, user, tenant);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private async authenticateSession(request : IncomingMessage, response : ServerResponse) : Promise<[User, Tenant] | undefined> {
|
|
112
|
-
if (!this.authenticator) return undefined;
|
|
113
|
-
return this.authenticator.authenticate(this.sessionCookie(request), request, response);
|
|
104
|
+
listen(port : number) : Promise<number> {
|
|
105
|
+
return this.publicServer.listen(port);
|
|
114
106
|
}
|
|
115
107
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
for (const cookie of cookies) {
|
|
119
|
-
const [name, ...value] = cookie.trim().split("=");
|
|
120
|
-
if (name === SESSION_COOKIE) return value.join("=");
|
|
121
|
-
}
|
|
122
|
-
return undefined;
|
|
108
|
+
listenInternal(port : number) : Promise<number> {
|
|
109
|
+
return this.internalServer.listen(port);
|
|
123
110
|
}
|
|
124
111
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
112
|
+
async close() : Promise<void> {
|
|
113
|
+
if (this.internalServer.listening) await this.internalServer.close();
|
|
114
|
+
await this.publicServer.close();
|
|
128
115
|
}
|
|
129
116
|
|
|
130
117
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {Request} from "./Request";
|
|
2
|
+
|
|
3
|
+
/* Runs before routing; may decorate the request or answer it. Returning
|
|
4
|
+
false stops the chain - the middleware has written the response. */
|
|
5
|
+
interface Middleware {
|
|
6
|
+
|
|
7
|
+
apply(request : Request) : Promise<boolean>;
|
|
8
|
+
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type { Middleware }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {randomUUID} from "node:crypto";
|
|
2
|
+
import {IncomingMessage, ServerResponse} from "node:http";
|
|
3
|
+
import {Tenant} from "../auth/Tenant";
|
|
4
|
+
import {User} from "../auth/User";
|
|
5
|
+
|
|
6
|
+
/* One inbound request: the parsed URL and body plus everything the server's
|
|
7
|
+
middleware decorates it with - session, user, tenant, ray trace id -
|
|
8
|
+
before it reaches a handler. Handlers respond through reply/replyHtml and
|
|
9
|
+
never touch the raw response unless they stream (the app proxy). */
|
|
10
|
+
class Request {
|
|
11
|
+
|
|
12
|
+
readonly rayId : string;
|
|
13
|
+
session? : string;
|
|
14
|
+
user? : User;
|
|
15
|
+
tenant? : Tenant;
|
|
16
|
+
|
|
17
|
+
private parsed : URL;
|
|
18
|
+
private segments : Array<string>;
|
|
19
|
+
|
|
20
|
+
constructor(private incoming : IncomingMessage, private response : ServerResponse,
|
|
21
|
+
rayId : string = randomUUID()) {
|
|
22
|
+
this.rayId = rayId;
|
|
23
|
+
this.parsed = new URL(incoming.url ?? "/", "http://localhost");
|
|
24
|
+
this.segments = this.parsed.pathname.split("/").filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get method() : string {
|
|
28
|
+
return this.incoming.method ?? "GET";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get url() : URL {
|
|
32
|
+
return this.parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get resource() : string | undefined {
|
|
36
|
+
return this.segments[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get id() : string | undefined {
|
|
40
|
+
return this.segments[1];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get subresource() : string | undefined {
|
|
44
|
+
return this.segments[2];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get raw() : IncomingMessage {
|
|
48
|
+
return this.incoming;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get rawResponse() : ServerResponse {
|
|
52
|
+
return this.response;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get handled() : boolean {
|
|
56
|
+
return this.response.writableEnded;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
header(name : string) : string | undefined {
|
|
60
|
+
const value = this.incoming.headers[name.toLowerCase()];
|
|
61
|
+
return value === undefined ? undefined : String(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
query(name : string) : string | undefined {
|
|
65
|
+
return this.parsed.searchParams.get(name) ?? undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
rawBody() : Promise<Buffer> {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const chunks : Array<Buffer> = [];
|
|
71
|
+
this.incoming.on("data", chunk => chunks.push(chunk));
|
|
72
|
+
this.incoming.on("error", reject);
|
|
73
|
+
this.incoming.on("end", () => resolve(Buffer.concat(chunks)));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async body() : Promise<any> {
|
|
78
|
+
const raw = (await this.rawBody()).toString();
|
|
79
|
+
return raw.length === 0 ? undefined : JSON.parse(raw);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
reply(status : number, body? : unknown) : void {
|
|
83
|
+
if (body === undefined) {
|
|
84
|
+
this.response.statusCode = status;
|
|
85
|
+
this.response.end();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.response.writeHead(status, { "content-type": "application/json" });
|
|
89
|
+
this.response.end(JSON.stringify(body));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
replyHtml(page : Buffer) : void {
|
|
93
|
+
this.response.writeHead(200, { "content-type": "text/html" });
|
|
94
|
+
this.response.end(page);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
notFound() : void {
|
|
98
|
+
this.reply(404, { error: "Not found" });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { Request }
|