emilsoftware-utilities 2.0.0-dev.82 → 2.0.0-dev.84

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.
Files changed (29) hide show
  1. package/dist/accessi/server/index.d.ts +4 -0
  2. package/dist/accessi/server/index.js +7 -1
  3. package/dist/accessi/server/index.js.map +1 -1
  4. package/dist/accessi/server/tenantRouter.d.ts +35 -0
  5. package/dist/accessi/server/tenantRouter.js +71 -0
  6. package/dist/accessi/server/tenantRouter.js.map +1 -0
  7. package/dist/accessi/server/tenantSupervisor.d.ts +60 -0
  8. package/dist/accessi/server/tenantSupervisor.js +296 -0
  9. package/dist/accessi/server/tenantSupervisor.js.map +1 -0
  10. package/dist/accessi/server/tenants.js +8 -0
  11. package/dist/accessi/server/tenants.js.map +1 -1
  12. package/dist/accessi-module/AccessiModule.js.map +1 -1
  13. package/dist/accessi-module/Console/console.js +1 -1
  14. package/dist/accessi-module/index.d.ts +1 -1
  15. package/dist/accessi-module/index.js +4 -3
  16. package/dist/accessi-module/index.js.map +1 -1
  17. package/dist/accessi-module/observability/metrics.js +13 -4
  18. package/dist/accessi-module/observability/metrics.js.map +1 -1
  19. package/dist/accessi-module/security/audit.d.ts +14 -4
  20. package/dist/accessi-module/security/audit.js +73 -48
  21. package/dist/accessi-module/security/audit.js.map +1 -1
  22. package/dist/accessi-module/tenancy/tenantContext.d.ts +15 -0
  23. package/dist/accessi-module/tenancy/tenantContext.js +26 -0
  24. package/dist/accessi-module/tenancy/tenantContext.js.map +1 -0
  25. package/dist/emilsoftware-utilities-2.0.0-dev.84.tgz +0 -0
  26. package/docs/adr/0002-multi-tenancy.md +9 -7
  27. package/docs/multi-tenancy.md +69 -3
  28. package/package.json +3 -3
  29. package/dist/emilsoftware-utilities-2.0.0-dev.82.tgz +0 -0
@@ -9,5 +9,9 @@ export * from "../../accessi-module";
9
9
  export * from "./standalone";
10
10
  export * from "./tenants";
11
11
  export * from "./tenantStores";
12
+ export * from "./tenantSupervisor";
13
+ export * from "./tenantRouter";
14
+ export { runWithAccessiTenant, getAccessiTenantContext, getAccessiTenantId, } from "../../accessi-module/tenancy/tenantContext";
15
+ export type { AccessiTenantContext } from "../../accessi-module/tenancy/tenantContext";
12
16
  export type { AccessiOptions as AccessiServerOptions } from "../../accessi-module";
13
17
  export { createAccessiOptionsFromEnv, createCatalogScriptsOptionsFromEnv, parseAccessiPreviousPublicKeys, } from "../../accessi-module/database-updates/accessiOptionsFromEnv";
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.parseAccessiPreviousPublicKeys = exports.createCatalogScriptsOptionsFromEnv = exports.createAccessiOptionsFromEnv = void 0;
17
+ exports.parseAccessiPreviousPublicKeys = exports.createCatalogScriptsOptionsFromEnv = exports.createAccessiOptionsFromEnv = exports.getAccessiTenantId = exports.getAccessiTenantContext = exports.runWithAccessiTenant = void 0;
18
18
  /**
19
19
  * Entry point server Accessi.
20
20
  *
@@ -26,6 +26,12 @@ __exportStar(require("../../accessi-module"), exports);
26
26
  __exportStar(require("./standalone"), exports);
27
27
  __exportStar(require("./tenants"), exports);
28
28
  __exportStar(require("./tenantStores"), exports);
29
+ __exportStar(require("./tenantSupervisor"), exports);
30
+ __exportStar(require("./tenantRouter"), exports);
31
+ var tenantContext_1 = require("../../accessi-module/tenancy/tenantContext");
32
+ Object.defineProperty(exports, "runWithAccessiTenant", { enumerable: true, get: function () { return tenantContext_1.runWithAccessiTenant; } });
33
+ Object.defineProperty(exports, "getAccessiTenantContext", { enumerable: true, get: function () { return tenantContext_1.getAccessiTenantContext; } });
34
+ Object.defineProperty(exports, "getAccessiTenantId", { enumerable: true, get: function () { return tenantContext_1.getAccessiTenantId; } });
29
35
  var accessiOptionsFromEnv_1 = require("../../accessi-module/database-updates/accessiOptionsFromEnv");
30
36
  Object.defineProperty(exports, "createAccessiOptionsFromEnv", { enumerable: true, get: function () { return accessiOptionsFromEnv_1.createAccessiOptionsFromEnv; } });
31
37
  Object.defineProperty(exports, "createCatalogScriptsOptionsFromEnv", { enumerable: true, get: function () { return accessiOptionsFromEnv_1.createCatalogScriptsOptionsFromEnv; } });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/accessi/server/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA;;;;;;GAMG;AACH,uDAAqC;AACrC,+CAA6B;AAC7B,4CAA0B;AAC1B,iDAA+B;AAE/B,qGAIqE;AAHnE,oIAAA,2BAA2B,OAAA;AAC3B,2IAAA,kCAAkC,OAAA;AAClC,uIAAA,8BAA8B,OAAA","sourcesContent":["/**\n * Entry point server Accessi.\n *\n * Questo e l'unico entry point che puo importare database, SMTP, migrazioni,\n * console e servizi Nest completi. I backend fratelli devono usare\n * `accessi/resource-server` e `accessi/client`.\n */\nexport * from \"../../accessi-module\";\nexport * from \"./standalone\";\nexport * from \"./tenants\";\nexport * from \"./tenantStores\";\nexport type { AccessiOptions as AccessiServerOptions } from \"../../accessi-module\";\nexport {\n createAccessiOptionsFromEnv,\n createCatalogScriptsOptionsFromEnv,\n parseAccessiPreviousPublicKeys,\n} from \"../../accessi-module/database-updates/accessiOptionsFromEnv\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/accessi/server/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA;;;;;;GAMG;AACH,uDAAqC;AACrC,+CAA6B;AAC7B,4CAA0B;AAC1B,iDAA+B;AAC/B,qDAAmC;AACnC,iDAA+B;AAC/B,4EAIoD;AAHlD,qHAAA,oBAAoB,OAAA;AACpB,wHAAA,uBAAuB,OAAA;AACvB,mHAAA,kBAAkB,OAAA;AAIpB,qGAIqE;AAHnE,oIAAA,2BAA2B,OAAA;AAC3B,2IAAA,kCAAkC,OAAA;AAClC,uIAAA,8BAA8B,OAAA","sourcesContent":["/**\n * Entry point server Accessi.\n *\n * Questo e l'unico entry point che puo importare database, SMTP, migrazioni,\n * console e servizi Nest completi. I backend fratelli devono usare\n * `accessi/resource-server` e `accessi/client`.\n */\nexport * from \"../../accessi-module\";\nexport * from \"./standalone\";\nexport * from \"./tenants\";\nexport * from \"./tenantStores\";\nexport * from \"./tenantSupervisor\";\nexport * from \"./tenantRouter\";\nexport {\n runWithAccessiTenant,\n getAccessiTenantContext,\n getAccessiTenantId,\n} from \"../../accessi-module/tenancy/tenantContext\";\nexport type { AccessiTenantContext } from \"../../accessi-module/tenancy/tenantContext\";\nexport type { AccessiOptions as AccessiServerOptions } from \"../../accessi-module\";\nexport {\n createAccessiOptionsFromEnv,\n createCatalogScriptsOptionsFromEnv,\n parseAccessiPreviousPublicKeys,\n} from \"../../accessi-module/database-updates/accessiOptionsFromEnv\";\n"]}
@@ -0,0 +1,35 @@
1
+ import type { Request, RequestHandler } from 'express';
2
+ import type { AccessiTenant, AccessiTenantRegistry } from './tenants';
3
+ /** Request arricchita dal router con il tenant risolto. */
4
+ export interface AccessiTenantAwareRequest extends Request {
5
+ accessiTenant?: AccessiTenant;
6
+ }
7
+ export interface CreateAccessiTenantRouterOptions {
8
+ registry: AccessiTenantRegistry;
9
+ /**
10
+ * Handler per tenant. Ogni handler deve essere indipendente (per esempio
11
+ * l'app Express di un `createAccessiService` gia inizializzato).
12
+ */
13
+ handlers: Record<string, RequestHandler> | Map<string, RequestHandler>;
14
+ /** Abilita l'header `x-accessi-tenant`. Default: false. */
15
+ allowHeader?: boolean;
16
+ /** Nome header tenant. Default `x-accessi-tenant`. */
17
+ headerName?: string;
18
+ /** Risposta quando nessun tenant corrisponde. Default: 404 JSON Accessi. */
19
+ onNotFound?: RequestHandler;
20
+ /** Risposta quando il tenant esiste ma non ha handler. Default: 501 JSON. */
21
+ onMissingHandler?: RequestHandler;
22
+ }
23
+ /**
24
+ * Router multi-tenant in-process (opt-in, fase 4 ADR).
25
+ *
26
+ * Risolve il tenant da host, path e header fidato, poi delega al suo handler
27
+ * dentro `runWithAccessiTenant`: audit e metriche emessi durante la richiesta
28
+ * ricevono il label `tenant`. Nessun fallback implicito: un host/path
29
+ * sconosciuto risponde 404 e non usa il primo tenant disponibile.
30
+ *
31
+ * Non sostituisce il processo-per-tenant come default: gli handler restano
32
+ * applicazioni indipendenti e le API globali (`authorizeAccessi`,
33
+ * `setAccessiAuthService`) non sono tenant-aware.
34
+ */
35
+ export declare function createAccessiTenantRouter(options: CreateAccessiTenantRouterOptions): RequestHandler;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAccessiTenantRouter = createAccessiTenantRouter;
4
+ const tenantContext_1 = require("../../accessi-module/tenancy/tenantContext");
5
+ function defaultHeader(request, name) {
6
+ const value = request.headers[name];
7
+ if (Array.isArray(value))
8
+ return value[0];
9
+ return typeof value === 'string' ? value : undefined;
10
+ }
11
+ function requestPath(request) {
12
+ if (typeof request.path === 'string' && request.path !== '')
13
+ return request.path;
14
+ const url = typeof request.url === 'string' ? request.url : '/';
15
+ const query = url.indexOf('?');
16
+ return query >= 0 ? url.slice(0, query) : url;
17
+ }
18
+ function getHandler(handlers, tenantId) {
19
+ if (handlers instanceof Map)
20
+ return handlers.get(tenantId);
21
+ return Object.prototype.hasOwnProperty.call(handlers, tenantId)
22
+ ? handlers[tenantId]
23
+ : undefined;
24
+ }
25
+ function sendTenantError(res, status, code, message) {
26
+ return res.status(status).json({
27
+ severity: 'error',
28
+ status,
29
+ statusCode: 2,
30
+ code,
31
+ error: code,
32
+ message,
33
+ });
34
+ }
35
+ /**
36
+ * Router multi-tenant in-process (opt-in, fase 4 ADR).
37
+ *
38
+ * Risolve il tenant da host, path e header fidato, poi delega al suo handler
39
+ * dentro `runWithAccessiTenant`: audit e metriche emessi durante la richiesta
40
+ * ricevono il label `tenant`. Nessun fallback implicito: un host/path
41
+ * sconosciuto risponde 404 e non usa il primo tenant disponibile.
42
+ *
43
+ * Non sostituisce il processo-per-tenant come default: gli handler restano
44
+ * applicazioni indipendenti e le API globali (`authorizeAccessi`,
45
+ * `setAccessiAuthService`) non sono tenant-aware.
46
+ */
47
+ function createAccessiTenantRouter(options) {
48
+ var _a, _b, _c;
49
+ if (!(options === null || options === void 0 ? void 0 : options.registry))
50
+ throw new Error('createAccessiTenantRouter: registry obbligatorio.');
51
+ if (!options.handlers)
52
+ throw new Error('createAccessiTenantRouter: handlers obbligatorio.');
53
+ const headerName = ((_a = options.headerName) !== null && _a !== void 0 ? _a : 'x-accessi-tenant').toLowerCase();
54
+ const notFound = (_b = options.onNotFound) !== null && _b !== void 0 ? _b : ((_req, res) => sendTenantError(res, 404, 'ACCESSI_TENANT_NOT_FOUND', 'Nessun tenant Accessi configurato per questa richiesta.'));
55
+ const missingHandler = (_c = options.onMissingHandler) !== null && _c !== void 0 ? _c : ((_req, res) => sendTenantError(res, 501, 'ACCESSI_TENANT_HANDLER_MISSING', 'Tenant risolto ma nessun handler configurato.'));
56
+ return (request, response, next) => {
57
+ const resolution = options.registry.resolve(Object.assign(Object.assign(Object.assign({}, (typeof request.headers.host === 'string' ? { host: request.headers.host } : {})), { path: requestPath(request) }), (defaultHeader(request, headerName) ? { header: defaultHeader(request, headerName) } : {})), options.allowHeader !== undefined ? { allowHeader: options.allowHeader } : undefined);
58
+ if (!resolution) {
59
+ notFound(request, response, next);
60
+ return;
61
+ }
62
+ const handler = getHandler(options.handlers, resolution.tenant.id);
63
+ if (!handler) {
64
+ missingHandler(request, response, next);
65
+ return;
66
+ }
67
+ request.accessiTenant = resolution.tenant;
68
+ return (0, tenantContext_1.runWithAccessiTenant)(resolution.tenant.id, () => handler(request, response, next));
69
+ };
70
+ }
71
+ //# sourceMappingURL=tenantRouter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenantRouter.js","sourceRoot":"","sources":["../../../src/accessi/server/tenantRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { NextFunction, Request, RequestHandler, Response } from 'express';\nimport type { AccessiTenant, AccessiTenantRegistry } from './tenants';\nimport { runWithAccessiTenant } from '../../accessi-module/tenancy/tenantContext';\n\n/** Request arricchita dal router con il tenant risolto. */\nexport interface AccessiTenantAwareRequest extends Request {\n accessiTenant?: AccessiTenant;\n}\n\nexport interface CreateAccessiTenantRouterOptions {\n registry: AccessiTenantRegistry;\n /**\n * Handler per tenant. Ogni handler deve essere indipendente (per esempio\n * l'app Express di un `createAccessiService` gia inizializzato).\n */\n handlers: Record<string, RequestHandler> | Map<string, RequestHandler>;\n /** Abilita l'header `x-accessi-tenant`. Default: false. */\n allowHeader?: boolean;\n /** Nome header tenant. Default `x-accessi-tenant`. */\n headerName?: string;\n /** Risposta quando nessun tenant corrisponde. Default: 404 JSON Accessi. */\n onNotFound?: RequestHandler;\n /** Risposta quando il tenant esiste ma non ha handler. Default: 501 JSON. */\n onMissingHandler?: RequestHandler;\n}\n\nfunction defaultHeader(request: Request, name: string): string | undefined {\n const value = request.headers[name];\n if (Array.isArray(value)) return value[0];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction requestPath(request: Request): string {\n if (typeof request.path === 'string' && request.path !== '') return request.path;\n const url = typeof request.url === 'string' ? request.url : '/';\n const query = url.indexOf('?');\n return query >= 0 ? url.slice(0, query) : url;\n}\n\nfunction getHandler(\n handlers: CreateAccessiTenantRouterOptions['handlers'],\n tenantId: string,\n): RequestHandler | undefined {\n if (handlers instanceof Map) return handlers.get(tenantId);\n return Object.prototype.hasOwnProperty.call(handlers, tenantId)\n ? handlers[tenantId]\n : undefined;\n}\n\nfunction sendTenantError(\n res: Response,\n status: number,\n code: string,\n message: string,\n): Response {\n return res.status(status).json({\n severity: 'error',\n status,\n statusCode: 2,\n code,\n error: code,\n message,\n });\n}\n\n/**\n * Router multi-tenant in-process (opt-in, fase 4 ADR).\n *\n * Risolve il tenant da host, path e header fidato, poi delega al suo handler\n * dentro `runWithAccessiTenant`: audit e metriche emessi durante la richiesta\n * ricevono il label `tenant`. Nessun fallback implicito: un host/path\n * sconosciuto risponde 404 e non usa il primo tenant disponibile.\n *\n * Non sostituisce il processo-per-tenant come default: gli handler restano\n * applicazioni indipendenti e le API globali (`authorizeAccessi`,\n * `setAccessiAuthService`) non sono tenant-aware.\n */\nexport function createAccessiTenantRouter(\n options: CreateAccessiTenantRouterOptions,\n): RequestHandler {\n if (!options?.registry) throw new Error('createAccessiTenantRouter: registry obbligatorio.');\n if (!options.handlers) throw new Error('createAccessiTenantRouter: handlers obbligatorio.');\n\n const headerName = (options.headerName ?? 'x-accessi-tenant').toLowerCase();\n const notFound = options.onNotFound ?? ((_req: Request, res: Response) => sendTenantError(\n res,\n 404,\n 'ACCESSI_TENANT_NOT_FOUND',\n 'Nessun tenant Accessi configurato per questa richiesta.',\n ));\n const missingHandler = options.onMissingHandler ?? ((_req: Request, res: Response) => sendTenantError(\n res,\n 501,\n 'ACCESSI_TENANT_HANDLER_MISSING',\n 'Tenant risolto ma nessun handler configurato.',\n ));\n\n return (request: Request, response: Response, next: NextFunction): void | Promise<void> => {\n const resolution = options.registry.resolve(\n {\n ...(typeof request.headers.host === 'string' ? { host: request.headers.host } : {}),\n path: requestPath(request),\n ...(defaultHeader(request, headerName) ? { header: defaultHeader(request, headerName) } : {}),\n },\n options.allowHeader !== undefined ? { allowHeader: options.allowHeader } : undefined,\n );\n\n if (!resolution) {\n notFound(request, response, next);\n return;\n }\n const handler = getHandler(options.handlers, resolution.tenant.id);\n if (!handler) {\n missingHandler(request, response, next);\n return;\n }\n\n (request as AccessiTenantAwareRequest).accessiTenant = resolution.tenant;\n return runWithAccessiTenant(resolution.tenant.id, () => handler(request, response, next)) as void | Promise<void>;\n };\n}\n"]}
@@ -0,0 +1,60 @@
1
+ import type { AccessiTenant } from './tenants';
2
+ /** Handle minimo di un processo figlio (compatibile con `child_process.ChildProcess`). */
3
+ export interface AccessiTenantProcessHandle {
4
+ readonly pid?: number;
5
+ kill(signal?: NodeJS.Signals | number): boolean;
6
+ once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
7
+ }
8
+ export type AccessiTenantSpawn = (tenant: AccessiTenant, port: number) => AccessiTenantProcessHandle;
9
+ export interface AccessiTenantSupervisorLoggerEvent {
10
+ level: 'info' | 'warn' | 'error';
11
+ message: string;
12
+ }
13
+ export interface AccessiTenantSupervisorOptions {
14
+ directory: string;
15
+ basePort: number;
16
+ spawn: AccessiTenantSpawn;
17
+ allowHeader?: boolean;
18
+ /** Intervallo di polling della cartella quando `watch()` e attivo. Default 1000 ms. */
19
+ pollIntervalMs?: number;
20
+ /** Attesa prima di applicare un reload dopo una modifica. Default 200 ms. */
21
+ debounceMs?: number;
22
+ /** Timeout per SIGTERM prima di SIGKILL. Default 5000 ms. */
23
+ stopTimeoutMs?: number;
24
+ logger?: (event: AccessiTenantSupervisorLoggerEvent) => void;
25
+ }
26
+ export interface AccessiTenantSupervisorState {
27
+ id: string;
28
+ port: number;
29
+ file: string;
30
+ signature: string;
31
+ }
32
+ export interface AccessiTenantReloadResult {
33
+ added: string[];
34
+ removed: string[];
35
+ restarted: string[];
36
+ unchanged: string[];
37
+ }
38
+ export interface AccessiTenantSupervisor {
39
+ start(): Promise<void>;
40
+ reload(): Promise<AccessiTenantReloadResult>;
41
+ stop(): Promise<void>;
42
+ state(): AccessiTenantSupervisorState[];
43
+ watch(): void;
44
+ unwatch(): void;
45
+ }
46
+ /** Firma della cartella: file + contenuto, ordinata per nome. */
47
+ export declare function accessiTenantDirectorySignature(directory: string): string;
48
+ /**
49
+ * Supervisore multi-tenant: avvia un processo per tenant e, con `watch()`,
50
+ * riapplica la configurazione quando i file `*.environment.ts` cambiano.
51
+ *
52
+ * Regole di reload:
53
+ * - un file invalido non tocca i tenant attivi;
54
+ * - i tenant invariati non vengono riavviati;
55
+ * - aggiunte/rimozioni/modifiche producono spawn/stop/restart;
56
+ * - la porta di un tenant esistente non cambia durante un restart.
57
+ */
58
+ export declare function createAccessiTenantSupervisor(options: AccessiTenantSupervisorOptions): AccessiTenantSupervisor;
59
+ /** Attende che una condizione diventi vera (utile nei test e nei runbook). */
60
+ export declare function waitForTenantState(supervisor: AccessiTenantSupervisor, predicate: (state: AccessiTenantSupervisorState[]) => boolean, timeoutMs?: number): Promise<void>;
@@ -0,0 +1,296 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.accessiTenantDirectorySignature = accessiTenantDirectorySignature;
13
+ exports.createAccessiTenantSupervisor = createAccessiTenantSupervisor;
14
+ exports.waitForTenantState = waitForTenantState;
15
+ const crypto_1 = require("crypto");
16
+ const fs_1 = require("fs");
17
+ const path_1 = require("path");
18
+ const tenants_1 = require("./tenants");
19
+ const ENVIRONMENT_FILE = /\.environment\.(ts|mts|cts|js|mjs|cjs)$/;
20
+ function fileSignature(file) {
21
+ try {
22
+ return (0, crypto_1.createHash)('sha256').update((0, fs_1.readFileSync)(file)).digest('hex');
23
+ }
24
+ catch (_a) {
25
+ return 'unreadable';
26
+ }
27
+ }
28
+ /** Firma della cartella: file + contenuto, ordinata per nome. */
29
+ function accessiTenantDirectorySignature(directory) {
30
+ let files;
31
+ try {
32
+ files = (0, fs_1.readdirSync)(directory, { withFileTypes: true })
33
+ .filter((entry) => entry.isFile() && ENVIRONMENT_FILE.test(entry.name))
34
+ .map((entry) => entry.name)
35
+ .sort();
36
+ }
37
+ catch (error) {
38
+ return `error:${error instanceof Error ? error.message : String(error)}`;
39
+ }
40
+ return (0, crypto_1.createHash)('sha256')
41
+ .update(files.map((file) => `${file}:${fileSignature((0, path_1.join)(directory, file))}`).join('|'))
42
+ .digest('hex');
43
+ }
44
+ function delay(ms) {
45
+ return new Promise((resolve) => setTimeout(resolve, ms));
46
+ }
47
+ function stopProcess(handle, timeoutMs) {
48
+ return __awaiter(this, void 0, void 0, function* () {
49
+ yield new Promise((resolve) => {
50
+ let settled = false;
51
+ const done = () => {
52
+ if (settled)
53
+ return;
54
+ settled = true;
55
+ resolve();
56
+ };
57
+ handle.once('exit', done);
58
+ try {
59
+ if (!handle.kill('SIGTERM'))
60
+ done();
61
+ }
62
+ catch (_a) {
63
+ done();
64
+ }
65
+ const timer = setTimeout(() => {
66
+ if (settled)
67
+ return;
68
+ try {
69
+ handle.kill('SIGKILL');
70
+ }
71
+ catch (_a) {
72
+ // Il processo potrebbe essere gia terminato.
73
+ }
74
+ done();
75
+ }, Math.max(50, timeoutMs));
76
+ if (typeof timer.unref === 'function')
77
+ timer.unref();
78
+ });
79
+ });
80
+ }
81
+ /**
82
+ * Supervisore multi-tenant: avvia un processo per tenant e, con `watch()`,
83
+ * riapplica la configurazione quando i file `*.environment.ts` cambiano.
84
+ *
85
+ * Regole di reload:
86
+ * - un file invalido non tocca i tenant attivi;
87
+ * - i tenant invariati non vengono riavviati;
88
+ * - aggiunte/rimozioni/modifiche producono spawn/stop/restart;
89
+ * - la porta di un tenant esistente non cambia durante un restart.
90
+ */
91
+ function createAccessiTenantSupervisor(options) {
92
+ var _a;
93
+ if (!(options === null || options === void 0 ? void 0 : options.directory))
94
+ throw new Error('createAccessiTenantSupervisor: directory obbligatoria.');
95
+ if (!Number.isInteger(options.basePort) || options.basePort < 1) {
96
+ throw new Error('createAccessiTenantSupervisor: basePort non valida.');
97
+ }
98
+ if (typeof options.spawn !== 'function') {
99
+ throw new Error('createAccessiTenantSupervisor: spawn obbligatorio.');
100
+ }
101
+ const log = (_a = options.logger) !== null && _a !== void 0 ? _a : (() => undefined);
102
+ const running = new Map();
103
+ let lastSignature = '';
104
+ let watchTimer;
105
+ let reloading = false;
106
+ let stopping = false;
107
+ let pendingReload;
108
+ const nextPort = () => {
109
+ const used = new Set([...running.values()].map((entry) => entry.port));
110
+ let port = options.basePort;
111
+ while (used.has(port))
112
+ port += 1;
113
+ return port;
114
+ };
115
+ const spawnTenant = (tenant, port, signature) => {
116
+ try {
117
+ const process = options.spawn(tenant, port);
118
+ return { tenant, port, signature, process };
119
+ }
120
+ catch (error) {
121
+ log({
122
+ level: 'error',
123
+ message: `Avvio tenant ${tenant.id} fallito: ${error instanceof Error ? error.message : String(error)}`,
124
+ });
125
+ return undefined;
126
+ }
127
+ };
128
+ const load = () => {
129
+ const registry = (0, tenants_1.loadAccessiTenants)(Object.assign({ directory: options.directory }, (options.allowHeader !== undefined ? { allowHeader: options.allowHeader } : {})));
130
+ const signatures = new Map();
131
+ for (const tenant of registry.tenants) {
132
+ signatures.set(tenant.id, fileSignature(tenant.environmentFile));
133
+ }
134
+ return { registry, signatures };
135
+ };
136
+ const remove = (id) => __awaiter(this, void 0, void 0, function* () {
137
+ var _a;
138
+ const entry = running.get(id);
139
+ if (!entry)
140
+ return;
141
+ running.delete(id);
142
+ log({ level: 'info', message: `Stop tenant ${id} (porta ${entry.port}).` });
143
+ yield stopProcess(entry.process, (_a = options.stopTimeoutMs) !== null && _a !== void 0 ? _a : 5000);
144
+ });
145
+ const startTenant = (tenant, signatures, port) => {
146
+ var _a, _b, _c;
147
+ const assignedPort = (_a = port !== null && port !== void 0 ? port : tenant.port) !== null && _a !== void 0 ? _a : nextPort();
148
+ if ([...running.values()].some((entry) => entry.port === assignedPort)) {
149
+ log({ level: 'error', message: `Porta ${assignedPort} gia in uso: tenant ${tenant.id} non avviato.` });
150
+ return false;
151
+ }
152
+ const entry = spawnTenant(tenant, assignedPort, (_b = signatures.get(tenant.id)) !== null && _b !== void 0 ? _b : '');
153
+ if (!entry)
154
+ return false;
155
+ running.set(tenant.id, entry);
156
+ log({
157
+ level: 'info',
158
+ message: `Avvio tenant ${tenant.id} sulla porta ${assignedPort} (pid ${(_c = entry.process.pid) !== null && _c !== void 0 ? _c : '?'}).`,
159
+ });
160
+ return true;
161
+ };
162
+ const apply = (registry, signatures) => __awaiter(this, void 0, void 0, function* () {
163
+ var _a;
164
+ const desired = new Map(registry.tenants.filter((tenant) => tenant.enabled).map((tenant) => [tenant.id, tenant]));
165
+ const result = { added: [], removed: [], restarted: [], unchanged: [] };
166
+ for (const id of [...running.keys()]) {
167
+ if (!desired.has(id)) {
168
+ yield remove(id);
169
+ result.removed.push(id);
170
+ }
171
+ }
172
+ for (const [id, tenant] of desired) {
173
+ const current = running.get(id);
174
+ if (!current) {
175
+ if (startTenant(tenant, signatures))
176
+ result.added.push(id);
177
+ continue;
178
+ }
179
+ const signature = (_a = signatures.get(id)) !== null && _a !== void 0 ? _a : '';
180
+ if (signature === current.signature) {
181
+ result.unchanged.push(id);
182
+ continue;
183
+ }
184
+ // Riavvio sulla stessa porta: il proxy non cambia configurazione.
185
+ const port = current.port;
186
+ yield remove(id);
187
+ if (startTenant(tenant, signatures, port))
188
+ result.restarted.push(id);
189
+ }
190
+ return result;
191
+ });
192
+ const unwatch = () => {
193
+ if (watchTimer)
194
+ clearInterval(watchTimer);
195
+ if (pendingReload)
196
+ clearTimeout(pendingReload);
197
+ watchTimer = undefined;
198
+ pendingReload = undefined;
199
+ };
200
+ const reload = () => __awaiter(this, void 0, void 0, function* () {
201
+ if (reloading || stopping)
202
+ return { added: [], removed: [], restarted: [], unchanged: [] };
203
+ reloading = true;
204
+ try {
205
+ const signature = accessiTenantDirectorySignature(options.directory);
206
+ let loaded;
207
+ try {
208
+ loaded = load();
209
+ }
210
+ catch (error) {
211
+ log({
212
+ level: 'error',
213
+ message: `Reload annullato, configurazione non valida: ${error instanceof Error ? error.message : String(error)}`,
214
+ });
215
+ lastSignature = signature;
216
+ return { added: [], removed: [], restarted: [], unchanged: [] };
217
+ }
218
+ const result = yield apply(loaded.registry, loaded.signatures);
219
+ lastSignature = signature;
220
+ log({
221
+ level: 'info',
222
+ message: `Reload tenant: +${result.added.length} -${result.removed.length} ~${result.restarted.length} =${result.unchanged.length}.`,
223
+ });
224
+ return result;
225
+ }
226
+ finally {
227
+ reloading = false;
228
+ }
229
+ });
230
+ const stop = () => __awaiter(this, void 0, void 0, function* () {
231
+ if (stopping)
232
+ return;
233
+ stopping = true;
234
+ unwatch();
235
+ yield Promise.all([...running.keys()].map((id) => remove(id)));
236
+ });
237
+ const watch = () => {
238
+ var _a, _b;
239
+ if (watchTimer)
240
+ return;
241
+ const interval = Math.max(100, (_a = options.pollIntervalMs) !== null && _a !== void 0 ? _a : 1000);
242
+ const debounce = Math.max(0, (_b = options.debounceMs) !== null && _b !== void 0 ? _b : 200);
243
+ watchTimer = setInterval(() => {
244
+ if (stopping || reloading)
245
+ return;
246
+ const signature = accessiTenantDirectorySignature(options.directory);
247
+ if (signature === lastSignature)
248
+ return;
249
+ if (pendingReload)
250
+ clearTimeout(pendingReload);
251
+ pendingReload = setTimeout(() => {
252
+ pendingReload = undefined;
253
+ void reload();
254
+ }, debounce);
255
+ }, interval);
256
+ if (typeof watchTimer.unref === 'function')
257
+ watchTimer.unref();
258
+ log({ level: 'info', message: `Watch attivo su ${options.directory} (${interval} ms).` });
259
+ };
260
+ return {
261
+ start() {
262
+ return __awaiter(this, void 0, void 0, function* () {
263
+ const { registry, signatures } = load();
264
+ lastSignature = accessiTenantDirectorySignature(options.directory);
265
+ yield apply(registry, signatures);
266
+ for (const tenant of registry.tenants.filter((entry) => !entry.enabled)) {
267
+ log({ level: 'info', message: `Tenant ${tenant.id} disabilitato: non avviato.` });
268
+ }
269
+ });
270
+ },
271
+ reload,
272
+ stop,
273
+ state() {
274
+ return [...running.values()].map((entry) => ({
275
+ id: entry.tenant.id,
276
+ port: entry.port,
277
+ file: entry.tenant.file,
278
+ signature: entry.signature,
279
+ }));
280
+ },
281
+ watch,
282
+ unwatch,
283
+ };
284
+ }
285
+ /** Attende che una condizione diventi vera (utile nei test e nei runbook). */
286
+ function waitForTenantState(supervisor_1, predicate_1) {
287
+ return __awaiter(this, arguments, void 0, function* (supervisor, predicate, timeoutMs = 5000) {
288
+ const deadline = Date.now() + timeoutMs;
289
+ while (!predicate(supervisor.state())) {
290
+ if (Date.now() > deadline)
291
+ throw new Error('timeout in attesa dello stato tenant');
292
+ yield delay(25);
293
+ }
294
+ });
295
+ }
296
+ //# sourceMappingURL=tenantSupervisor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenantSupervisor.js","sourceRoot":"","sources":["../../../src/accessi/server/tenantSupervisor.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'crypto';\nimport { readdirSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport type { AccessiTenant, AccessiTenantRegistry } from './tenants';\nimport { loadAccessiTenants } from './tenants';\n\n/** Handle minimo di un processo figlio (compatibile con `child_process.ChildProcess`). */\nexport interface AccessiTenantProcessHandle {\n readonly pid?: number;\n kill(signal?: NodeJS.Signals | number): boolean;\n once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;\n}\n\nexport type AccessiTenantSpawn = (tenant: AccessiTenant, port: number) => AccessiTenantProcessHandle;\n\nexport interface AccessiTenantSupervisorLoggerEvent {\n level: 'info' | 'warn' | 'error';\n message: string;\n}\n\nexport interface AccessiTenantSupervisorOptions {\n directory: string;\n basePort: number;\n spawn: AccessiTenantSpawn;\n allowHeader?: boolean;\n /** Intervallo di polling della cartella quando `watch()` e attivo. Default 1000 ms. */\n pollIntervalMs?: number;\n /** Attesa prima di applicare un reload dopo una modifica. Default 200 ms. */\n debounceMs?: number;\n /** Timeout per SIGTERM prima di SIGKILL. Default 5000 ms. */\n stopTimeoutMs?: number;\n logger?: (event: AccessiTenantSupervisorLoggerEvent) => void;\n}\n\nexport interface AccessiTenantSupervisorState {\n id: string;\n port: number;\n file: string;\n signature: string;\n}\n\nexport interface AccessiTenantReloadResult {\n added: string[];\n removed: string[];\n restarted: string[];\n unchanged: string[];\n}\n\nexport interface AccessiTenantSupervisor {\n start(): Promise<void>;\n reload(): Promise<AccessiTenantReloadResult>;\n stop(): Promise<void>;\n state(): AccessiTenantSupervisorState[];\n watch(): void;\n unwatch(): void;\n}\n\ninterface RunningTenant {\n tenant: AccessiTenant;\n port: number;\n signature: string;\n process: AccessiTenantProcessHandle;\n}\n\nconst ENVIRONMENT_FILE = /\\.environment\\.(ts|mts|cts|js|mjs|cjs)$/;\n\nfunction fileSignature(file: string): string {\n try {\n return createHash('sha256').update(readFileSync(file)).digest('hex');\n } catch {\n return 'unreadable';\n }\n}\n\n/** Firma della cartella: file + contenuto, ordinata per nome. */\nexport function accessiTenantDirectorySignature(directory: string): string {\n let files: string[];\n try {\n files = readdirSync(directory, { withFileTypes: true })\n .filter((entry) => entry.isFile() && ENVIRONMENT_FILE.test(entry.name))\n .map((entry) => entry.name)\n .sort();\n } catch (error) {\n return `error:${error instanceof Error ? error.message : String(error)}`;\n }\n return createHash('sha256')\n .update(files.map((file) => `${file}:${fileSignature(join(directory, file))}`).join('|'))\n .digest('hex');\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function stopProcess(\n handle: AccessiTenantProcessHandle,\n timeoutMs: number,\n): Promise<void> {\n await new Promise<void>((resolve) => {\n let settled = false;\n const done = () => {\n if (settled) return;\n settled = true;\n resolve();\n };\n handle.once('exit', done);\n try {\n if (!handle.kill('SIGTERM')) done();\n } catch {\n done();\n }\n const timer = setTimeout(() => {\n if (settled) return;\n try {\n handle.kill('SIGKILL');\n } catch {\n // Il processo potrebbe essere gia terminato.\n }\n done();\n }, Math.max(50, timeoutMs));\n if (typeof timer.unref === 'function') timer.unref();\n });\n}\n\n/**\n * Supervisore multi-tenant: avvia un processo per tenant e, con `watch()`,\n * riapplica la configurazione quando i file `*.environment.ts` cambiano.\n *\n * Regole di reload:\n * - un file invalido non tocca i tenant attivi;\n * - i tenant invariati non vengono riavviati;\n * - aggiunte/rimozioni/modifiche producono spawn/stop/restart;\n * - la porta di un tenant esistente non cambia durante un restart.\n */\nexport function createAccessiTenantSupervisor(\n options: AccessiTenantSupervisorOptions,\n): AccessiTenantSupervisor {\n if (!options?.directory) throw new Error('createAccessiTenantSupervisor: directory obbligatoria.');\n if (!Number.isInteger(options.basePort) || options.basePort < 1) {\n throw new Error('createAccessiTenantSupervisor: basePort non valida.');\n }\n if (typeof options.spawn !== 'function') {\n throw new Error('createAccessiTenantSupervisor: spawn obbligatorio.');\n }\n\n const log = options.logger ?? (() => undefined);\n const running = new Map<string, RunningTenant>();\n let lastSignature = '';\n let watchTimer: NodeJS.Timeout | undefined;\n let reloading = false;\n let stopping = false;\n let pendingReload: NodeJS.Timeout | undefined;\n\n const nextPort = (): number => {\n const used = new Set([...running.values()].map((entry) => entry.port));\n let port = options.basePort;\n while (used.has(port)) port += 1;\n return port;\n };\n\n const spawnTenant = (tenant: AccessiTenant, port: number, signature: string): RunningTenant | undefined => {\n try {\n const process = options.spawn(tenant, port);\n return { tenant, port, signature, process };\n } catch (error) {\n log({\n level: 'error',\n message: `Avvio tenant ${tenant.id} fallito: ${error instanceof Error ? error.message : String(error)}`,\n });\n return undefined;\n }\n };\n\n const load = (): { registry: AccessiTenantRegistry; signatures: Map<string, string> } => {\n const registry = loadAccessiTenants({\n directory: options.directory,\n ...(options.allowHeader !== undefined ? { allowHeader: options.allowHeader } : {}),\n });\n const signatures = new Map<string, string>();\n for (const tenant of registry.tenants) {\n signatures.set(tenant.id, fileSignature(tenant.environmentFile));\n }\n return { registry, signatures };\n };\n\n const remove = async (id: string): Promise<void> => {\n const entry = running.get(id);\n if (!entry) return;\n running.delete(id);\n log({ level: 'info', message: `Stop tenant ${id} (porta ${entry.port}).` });\n await stopProcess(entry.process, options.stopTimeoutMs ?? 5000);\n };\n\n const startTenant = (tenant: AccessiTenant, signatures: Map<string, string>, port?: number): boolean => {\n const assignedPort = port ?? tenant.port ?? nextPort();\n if ([...running.values()].some((entry) => entry.port === assignedPort)) {\n log({ level: 'error', message: `Porta ${assignedPort} gia in uso: tenant ${tenant.id} non avviato.` });\n return false;\n }\n const entry = spawnTenant(tenant, assignedPort, signatures.get(tenant.id) ?? '');\n if (!entry) return false;\n running.set(tenant.id, entry);\n log({\n level: 'info',\n message: `Avvio tenant ${tenant.id} sulla porta ${assignedPort} (pid ${entry.process.pid ?? '?'}).`,\n });\n return true;\n };\n\n const apply = async (registry: AccessiTenantRegistry, signatures: Map<string, string>): Promise<AccessiTenantReloadResult> => {\n const desired = new Map(registry.tenants.filter((tenant) => tenant.enabled).map((tenant) => [tenant.id, tenant]));\n const result: AccessiTenantReloadResult = { added: [], removed: [], restarted: [], unchanged: [] };\n\n for (const id of [...running.keys()]) {\n if (!desired.has(id)) {\n await remove(id);\n result.removed.push(id);\n }\n }\n\n for (const [id, tenant] of desired) {\n const current = running.get(id);\n if (!current) {\n if (startTenant(tenant, signatures)) result.added.push(id);\n continue;\n }\n const signature = signatures.get(id) ?? '';\n if (signature === current.signature) {\n result.unchanged.push(id);\n continue;\n }\n\n // Riavvio sulla stessa porta: il proxy non cambia configurazione.\n const port = current.port;\n await remove(id);\n if (startTenant(tenant, signatures, port)) result.restarted.push(id);\n }\n\n return result;\n };\n\n const unwatch = (): void => {\n if (watchTimer) clearInterval(watchTimer);\n if (pendingReload) clearTimeout(pendingReload);\n watchTimer = undefined;\n pendingReload = undefined;\n };\n\n const reload = async (): Promise<AccessiTenantReloadResult> => {\n if (reloading || stopping) return { added: [], removed: [], restarted: [], unchanged: [] };\n reloading = true;\n try {\n const signature = accessiTenantDirectorySignature(options.directory);\n let loaded;\n try {\n loaded = load();\n } catch (error) {\n log({\n level: 'error',\n message: `Reload annullato, configurazione non valida: ${error instanceof Error ? error.message : String(error)}`,\n });\n lastSignature = signature;\n return { added: [], removed: [], restarted: [], unchanged: [] };\n }\n const result = await apply(loaded.registry, loaded.signatures);\n lastSignature = signature;\n log({\n level: 'info',\n message: `Reload tenant: +${result.added.length} -${result.removed.length} ~${result.restarted.length} =${result.unchanged.length}.`,\n });\n return result;\n } finally {\n reloading = false;\n }\n };\n\n const stop = async (): Promise<void> => {\n if (stopping) return;\n stopping = true;\n unwatch();\n await Promise.all([...running.keys()].map((id) => remove(id)));\n };\n\n const watch = (): void => {\n if (watchTimer) return;\n const interval = Math.max(100, options.pollIntervalMs ?? 1000);\n const debounce = Math.max(0, options.debounceMs ?? 200);\n watchTimer = setInterval(() => {\n if (stopping || reloading) return;\n const signature = accessiTenantDirectorySignature(options.directory);\n if (signature === lastSignature) return;\n if (pendingReload) clearTimeout(pendingReload);\n pendingReload = setTimeout(() => {\n pendingReload = undefined;\n void reload();\n }, debounce);\n }, interval);\n if (typeof watchTimer.unref === 'function') watchTimer.unref();\n log({ level: 'info', message: `Watch attivo su ${options.directory} (${interval} ms).` });\n };\n\n return {\n async start() {\n const { registry, signatures } = load();\n lastSignature = accessiTenantDirectorySignature(options.directory);\n await apply(registry, signatures);\n for (const tenant of registry.tenants.filter((entry) => !entry.enabled)) {\n log({ level: 'info', message: `Tenant ${tenant.id} disabilitato: non avviato.` });\n }\n },\n reload,\n stop,\n state() {\n return [...running.values()].map((entry) => ({\n id: entry.tenant.id,\n port: entry.port,\n file: entry.tenant.file,\n signature: entry.signature,\n }));\n },\n watch,\n unwatch,\n };\n}\n\n/** Attende che una condizione diventi vera (utile nei test e nei runbook). */\nexport async function waitForTenantState(\n supervisor: AccessiTenantSupervisor,\n predicate: (state: AccessiTenantSupervisorState[]) => boolean,\n timeoutMs = 5000,\n): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n while (!predicate(supervisor.state())) {\n if (Date.now() > deadline) throw new Error('timeout in attesa dello stato tenant');\n await delay(25);\n }\n}\n"]}
@@ -249,6 +249,7 @@ function loadAccessiTenants(options) {
249
249
  const pathsSeen = new Map();
250
250
  const jwtSecrets = new Map();
251
251
  const issuerAudience = new Map();
252
+ const portsSeen = new Map();
252
253
  for (const tenant of tenants) {
253
254
  if (byId.has(tenant.id)) {
254
255
  issues.push(`Tenant duplicato "${tenant.id}" (${tenant.file} e ${(_a = byId.get(tenant.id)) === null || _a === void 0 ? void 0 : _a.file}).`);
@@ -270,6 +271,13 @@ function loadAccessiTenants(options) {
270
271
  else
271
272
  pathsSeen.set(tenant.path, tenant.id);
272
273
  }
274
+ if (tenant.port !== undefined) {
275
+ const existing = portsSeen.get(tenant.port);
276
+ if (existing)
277
+ issues.push(`Porta ${tenant.port} assegnata a "${existing}" e "${tenant.id}".`);
278
+ else
279
+ portsSeen.set(tenant.port, tenant.id);
280
+ }
273
281
  // Un segreto JWT condiviso rende un token valido su piu tenant.
274
282
  const signingKey = (_b = tenant.options.jwtOptions.privateKey) !== null && _b !== void 0 ? _b : tenant.options.jwtOptions.secret;
275
283
  const fingerprintKey = `${(_c = tenant.options.jwtOptions.algorithm) !== null && _c !== void 0 ? _c : 'HS256'}|${signingKey}`;