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

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.
@@ -0,0 +1,45 @@
1
+ # Template di deployment multi-tenant
2
+
3
+ File pronti da adattare. Non contengono segreti reali: usare `<placeholder>` o
4
+ variabili d'ambiente/secret manager.
5
+
6
+ | Percorso | Uso |
7
+ | --- | --- |
8
+ | `systemd/accessi-tenant@.service` | unità template, una istanza per tenant |
9
+ | `systemd/tenant.secrets.example` | soli segreti, permessi `0600` |
10
+ | `docker/docker-compose.yml` | due tenant con volumi config read-only |
11
+ | `nginx/accessi-tenants.conf` | routing per host e variante path |
12
+ | `traefik/dynamic.yml` | routing host, header e health check |
13
+
14
+ ## Flusso
15
+
16
+ 1. Creare `tenants/<id>.environment.ts` (vedi `docs/multi-tenancy.md`).
17
+ 2. Validare: `npm run accessi:tenants -- --dir ./tenants --check`.
18
+ 3. Preparare i segreti fuori dal repository (`/etc/accessi/tenants/<id>.secrets`
19
+ o `secrets/<id>.env` con permessi `0600`).
20
+ 4. Avviare il processo con `ACCESSI_TENANT_ENVIRONMENT` e, se serve,
21
+ `PORT`/`ACCESSI_TENANT_ID`.
22
+ 5. Configurare il proxy sull'host o sul path del tenant.
23
+ 6. Verificare `/health/live`, `/health/ready` e `/health/metrics`.
24
+
25
+ La porta predefinita è `tenant.port`; `PORT` ha precedenza. I template non
26
+ pubblicano le porte interne: il proxy è l'unico punto esposto.
27
+
28
+ ## Variante path
29
+
30
+ Per `tenant.path = '/acme'` il proxy deve rimuovere `/acme` prima di inoltrare
31
+ (vedi `nginx/accessi-tenants.conf` e `traefik/dynamic.yml`). In alternativa
32
+ servire ogni tenant su un host dedicato.
33
+
34
+ ## Sicurezza
35
+
36
+ - Non committare `.secrets`/`secrets/*.env`.
37
+ - Montare la cartella configurazione in read-only.
38
+ - `read_only: true`, `no-new-privileges`, utente non root (immagine
39
+ personalizzata) e `ProtectSystem=strict` nell'unità systemd.
40
+ - Non usare l'header `x-accessi-tenant` dal client: solo il proxy può
41
+ impostarlo, con `allowHeader: true` lato router.
42
+ - Un processo per tenant: non condividere `AccessiModule` tra tenant diversi.
43
+
44
+ La CI esegue `npm run check:deploy` per verificare presenza e coerenza dei
45
+ template e l'assenza di segreti.
@@ -0,0 +1,51 @@
1
+ # Accessi multi-tenant con Docker Compose.
2
+ #
3
+ # I file *.environment.ts sono montati read-only; i segreti arrivano da
4
+ # env_file esterni al repository. La porta viene da tenant.port; PORT puo
5
+ # sovrascriverla.
6
+ version: "3.9"
7
+
8
+ x-accessi-tenant: &accessi-tenant
9
+ image: emilsoftware/accessi:2
10
+ restart: unless-stopped
11
+ read_only: true
12
+ tmpfs:
13
+ - /tmp
14
+ security_opt:
15
+ - no-new-privileges:true
16
+ healthcheck:
17
+ test:
18
+ - CMD-SHELL
19
+ - >-
20
+ node -e "fetch('http://127.0.0.1:' + (process.env.PORT || '3200') + '/health/live')
21
+ .then((response) => process.exit(response.ok ? 0 : 1))
22
+ .catch(() => process.exit(1))"
23
+ interval: 15s
24
+ timeout: 5s
25
+ retries: 5
26
+ start_period: 30s
27
+
28
+ services:
29
+ accessi-acme:
30
+ <<: *accessi-tenant
31
+ environment:
32
+ ACCESSI_TENANT_ENVIRONMENT: /config/acme.environment.ts
33
+ ACCESSI_TENANT_ID: acme
34
+ env_file:
35
+ - ./secrets/acme.env
36
+ volumes:
37
+ - ./tenants:/config:ro
38
+ ports:
39
+ - "127.0.0.1:3201:3201"
40
+
41
+ accessi-beta:
42
+ <<: *accessi-tenant
43
+ environment:
44
+ ACCESSI_TENANT_ENVIRONMENT: /config/beta.environment.ts
45
+ ACCESSI_TENANT_ID: beta
46
+ env_file:
47
+ - ./secrets/beta.env
48
+ volumes:
49
+ - ./tenants:/config:ro
50
+ ports:
51
+ - "127.0.0.1:3202:3202"
@@ -0,0 +1,41 @@
1
+ # Accessi multi-tenant dietro Nginx.
2
+ #
3
+ # Ogni tenant ha un processo sulla propria porta interna. Il proxy instrada per
4
+ # host e non espone mai le porte interne. Sostituire i domini e le porte.
5
+
6
+ map $host $accessi_tenant_upstream {
7
+ accessi.acme.example.com http://127.0.0.1:3201;
8
+ accessi.beta.example.com http://127.0.0.1:3202;
9
+ default http://127.0.0.1:3201;
10
+ }
11
+
12
+ server {
13
+ listen 443 ssl http2;
14
+ server_name accessi.acme.example.com accessi.beta.example.com;
15
+
16
+ ssl_certificate /etc/letsencrypt/live/accessi.example.com/fullchain.pem;
17
+ ssl_certificate_key /etc/letsencrypt/live/accessi.example.com/privkey.pem;
18
+
19
+ client_max_body_size 10m;
20
+
21
+ location / {
22
+ proxy_pass $accessi_tenant_upstream;
23
+ proxy_http_version 1.1;
24
+ proxy_set_header Host $host;
25
+ proxy_set_header X-Real-IP $remote_addr;
26
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
27
+ proxy_set_header X-Forwarded-Proto $scheme;
28
+ proxy_set_header X-Forwarded-Host $host;
29
+ proxy_read_timeout 60s;
30
+ }
31
+ }
32
+
33
+ # Variante path-based: il prefix /acme viene rimosso prima di inoltrare ad
34
+ # Accessi, che serve il tenant alla radice.
35
+ #
36
+ # location /acme/ {
37
+ # rewrite ^/acme/(.*)$ /$1 break;
38
+ # proxy_pass http://127.0.0.1:3201;
39
+ # proxy_set_header Host $host;
40
+ # proxy_set_header X-Forwarded-Proto $scheme;
41
+ # }
@@ -0,0 +1,46 @@
1
+ # Accessi multi-tenant con systemd
2
+ #
3
+ # Installazione:
4
+ # cp deploy/systemd/accessi-tenant@.service /etc/systemd/system/
5
+ # mkdir -p /etc/accessi/tenants
6
+ # cp tenants/acme.environment.ts /etc/accessi/tenants/acme.environment.ts
7
+ # install -m 0600 deploy/systemd/tenant.secrets.example /etc/accessi/tenants/acme.secrets
8
+ # systemctl daemon-reload
9
+ # systemctl enable --now accessi-tenant@acme
10
+ #
11
+ # La porta viene letta da tenant.port nel file *.environment.ts. Per forzarla:
12
+ # systemctl edit accessi-tenant@acme
13
+ # [Service]
14
+ # Environment=PORT=3201
15
+
16
+ [Unit]
17
+ Description=Accessi Service tenant %i
18
+ Documentation=file:///opt/accessi/docs/multi-tenancy.md
19
+ After=network-online.target
20
+ Wants=network-online.target
21
+
22
+ [Service]
23
+ Type=simple
24
+ WorkingDirectory=/opt/accessi
25
+ # Solo segreti (password DB, SMTP, chiavi JWT): il manifest tipizzato e' separato.
26
+ EnvironmentFile=-/etc/accessi/tenants/%i.secrets
27
+ Environment=ACCESSI_TENANT_ENVIRONMENT=/etc/accessi/tenants/%i.environment.ts
28
+ Environment=ACCESSI_TENANT_ID=%i
29
+
30
+ ExecStart=/usr/bin/node /opt/accessi/dist/scripts/startAccessiTenantService.js
31
+ Restart=on-failure
32
+ RestartSec=5
33
+ TimeoutStopSec=30
34
+ KillSignal=SIGTERM
35
+
36
+ User=accessi
37
+ Group=accessi
38
+ NoNewPrivileges=true
39
+ PrivateTmp=true
40
+ ProtectSystem=strict
41
+ ProtectHome=true
42
+ ReadWritePaths=/var/lib/accessi
43
+ LimitNOFILE=65536
44
+
45
+ [Install]
46
+ WantedBy=multi-user.target
@@ -0,0 +1,11 @@
1
+ # Esempio di file segreti per un tenant systemd.
2
+ # Copiare in /etc/accessi/tenants/<id>.secrets con permessi 0600.
3
+ # Non committare valori reali.
4
+
5
+ ACME_DB_PASSWORD=<db-password>
6
+ ACME_JWT_SECRET=<jwt-secret-almeno-32-caratteri>
7
+ ACME_JWT_PRIVATE_KEY=<pem-su-una-riga-con-\n>
8
+ ACME_JWT_PUBLIC_KEY=<pem-su-una-riga-con-\n>
9
+ ACME_ENCRYPTION_KEY=<32-caratteri-casuali>
10
+ ACME_SMTP_USER=<smtp-user>
11
+ ACME_SMTP_PASSWORD=<smtp-password>
@@ -0,0 +1,57 @@
1
+ # Accessi multi-tenant con Traefik (file provider).
2
+ #
3
+ # I servizi Accessi ascoltano su porte interne; Traefik termina TLS e instrada
4
+ # per host. Per la variante path usare un middleware StripPrefix.
5
+
6
+ http:
7
+ routers:
8
+ accessi-acme:
9
+ rule: Host(`accessi.acme.example.com`)
10
+ entryPoints:
11
+ - websecure
12
+ tls: {}
13
+ service: accessi-acme
14
+ middlewares:
15
+ - accessi-headers
16
+
17
+ accessi-beta:
18
+ rule: Host(`accessi.beta.example.com`)
19
+ entryPoints:
20
+ - websecure
21
+ tls: {}
22
+ service: accessi-beta
23
+ middlewares:
24
+ - accessi-headers
25
+
26
+ middlewares:
27
+ accessi-headers:
28
+ headers:
29
+ stsSeconds: 31536000
30
+ stsIncludeSubdomains: true
31
+ contentTypeNosniff: true
32
+ referrerPolicy: no-referrer
33
+ customResponseHeaders:
34
+ Permissions-Policy: "camera=(), microphone=(), geolocation=()"
35
+ # Variante path-based:
36
+ # accessi-strip-acme:
37
+ # stripPrefix:
38
+ # prefixes:
39
+ # - /acme
40
+
41
+ services:
42
+ accessi-acme:
43
+ loadBalancer:
44
+ healthCheck:
45
+ path: /health/ready
46
+ interval: 15s
47
+ timeout: 3s
48
+ servers:
49
+ - url: http://127.0.0.1:3201
50
+ accessi-beta:
51
+ loadBalancer:
52
+ healthCheck:
53
+ path: /health/ready
54
+ interval: 15s
55
+ timeout: 3s
56
+ servers:
57
+ - url: http://127.0.0.1:3202
@@ -8,5 +8,6 @@
8
8
  export * from "../../accessi-module";
9
9
  export * from "./standalone";
10
10
  export * from "./tenants";
11
+ export * from "./tenantStores";
11
12
  export type { AccessiOptions as AccessiServerOptions } from "../../accessi-module";
12
13
  export { createAccessiOptionsFromEnv, createCatalogScriptsOptionsFromEnv, parseAccessiPreviousPublicKeys, } from "../../accessi-module/database-updates/accessiOptionsFromEnv";
@@ -25,6 +25,7 @@ exports.parseAccessiPreviousPublicKeys = exports.createCatalogScriptsOptionsFrom
25
25
  __exportStar(require("../../accessi-module"), exports);
26
26
  __exportStar(require("./standalone"), exports);
27
27
  __exportStar(require("./tenants"), exports);
28
+ __exportStar(require("./tenantStores"), exports);
28
29
  var accessiOptionsFromEnv_1 = require("../../accessi-module/database-updates/accessiOptionsFromEnv");
29
30
  Object.defineProperty(exports, "createAccessiOptionsFromEnv", { enumerable: true, get: function () { return accessiOptionsFromEnv_1.createAccessiOptionsFromEnv; } });
30
31
  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;AAE1B,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 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;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"]}
@@ -0,0 +1,74 @@
1
+ import type { AccessiOptions } from '../../accessi-module/AccessiModule';
2
+ import type { RateLimitStore } from '../../accessi-module/security/rateLimitStore';
3
+ import type { ConsoleEntryStore } from '../../accessi-module/security/consoleEntry';
4
+ import type { SessionRevocationStore } from '../../accessi-module/security/sessionRevocation';
5
+ import type { AccessTokenRevocationStore } from '../../accessi-module/security/accessTokenRevocation';
6
+ import type { RefreshTokenRotationStore } from '../../accessi-module/security/refreshTokenRotation';
7
+ /** Client Redis minimo accettato dagli store Accessi. */
8
+ export interface AccessiTenantRedisClient {
9
+ eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;
10
+ }
11
+ /** Prefissi Redis per singolo store, tutti relativi al tenant. */
12
+ export interface AccessiTenantStorePrefixes {
13
+ base?: string;
14
+ rateLimit?: string;
15
+ serviceTokenRateLimit?: string;
16
+ sessionRevocation?: string;
17
+ accessTokenRevocation?: string;
18
+ refreshTokenRotation?: string;
19
+ refreshTokenFamily?: string;
20
+ consoleEntry?: string;
21
+ }
22
+ export interface ResolvedAccessiTenantStorePrefixes {
23
+ base: string;
24
+ rateLimit: string;
25
+ serviceTokenRateLimit: string;
26
+ sessionRevocation: string;
27
+ accessTokenRevocation: string;
28
+ refreshTokenRotation: string;
29
+ refreshTokenFamily: string;
30
+ consoleEntry: string;
31
+ }
32
+ export interface CreateAccessiTenantStoresOptions {
33
+ tenantId: string;
34
+ client: AccessiTenantRedisClient;
35
+ /** Segreto per cifrare i ticket console a riposo. Usare un segreto del tenant. */
36
+ encryptionKey: string | Buffer;
37
+ /** Base prefix. Default: `accessi:<tenantId>:`. */
38
+ prefix?: string;
39
+ prefixes?: AccessiTenantStorePrefixes;
40
+ /** TTL revoca sessioni. Default dello store (365 giorni). */
41
+ sessionRevocationTtlMs?: number;
42
+ /** TTL revoca jti. Default dello store (24 ore). */
43
+ accessTokenRevocationTtlMs?: number;
44
+ consoleEntryMaxIssueAttempts?: number;
45
+ /** Attiva la rotazione refresh token nell'options risultante. Default: true. */
46
+ enableRefreshTokenRotation?: boolean;
47
+ }
48
+ export interface AccessiTenantStores {
49
+ tenantId: string;
50
+ prefixes: ResolvedAccessiTenantStorePrefixes;
51
+ rateLimit: RateLimitStore;
52
+ serviceTokenRateLimit: RateLimitStore;
53
+ sessionRevocation: SessionRevocationStore;
54
+ accessTokenRevocation: AccessTokenRevocationStore;
55
+ refreshTokenRotation: RefreshTokenRotationStore;
56
+ consoleEntry: ConsoleEntryStore;
57
+ /** Collega tutti gli store alle opzioni Accessi del tenant. */
58
+ applyTo(options: AccessiOptions): AccessiOptions;
59
+ }
60
+ /**
61
+ * Prefissi Redis isolati per tenant.
62
+ *
63
+ * Ogni store ha un namespace distinto: condividere un prefisso tra tenant
64
+ * significherebbe condividere rate limit, revoche o refresh token.
65
+ */
66
+ export declare function getAccessiTenantStorePrefixes(tenantId: string, overrides?: AccessiTenantStorePrefixes): ResolvedAccessiTenantStorePrefixes;
67
+ /**
68
+ * Crea tutti gli store Redis di un tenant con prefissi gia isolati.
69
+ *
70
+ * `applyTo` restituisce una copia delle opzioni con gli store collegati:
71
+ * `publicAuthRateLimit`, `serviceTokens`, `sessionRevocation`,
72
+ * `accessTokenRevocation`, `refreshTokenRotation` e `consoleEntry`.
73
+ */
74
+ export declare function createAccessiTenantStores(options: CreateAccessiTenantStoresOptions): AccessiTenantStores;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAccessiTenantStorePrefixes = getAccessiTenantStorePrefixes;
4
+ exports.createAccessiTenantStores = createAccessiTenantStores;
5
+ const tenants_1 = require("./tenants");
6
+ const rateLimitStore_1 = require("../../accessi-module/security/rateLimitStore");
7
+ const consoleEntry_1 = require("../../accessi-module/security/consoleEntry");
8
+ const sessionRevocation_1 = require("../../accessi-module/security/sessionRevocation");
9
+ const accessTokenRevocation_1 = require("../../accessi-module/security/accessTokenRevocation");
10
+ const refreshTokenRotation_1 = require("../../accessi-module/security/refreshTokenRotation");
11
+ /**
12
+ * Prefissi Redis isolati per tenant.
13
+ *
14
+ * Ogni store ha un namespace distinto: condividere un prefisso tra tenant
15
+ * significherebbe condividere rate limit, revoche o refresh token.
16
+ */
17
+ function getAccessiTenantStorePrefixes(tenantId, overrides = {}) {
18
+ var _a, _b, _c, _d, _e, _f, _g, _h;
19
+ const base = (_a = overrides.base) !== null && _a !== void 0 ? _a : (0, tenants_1.getAccessiTenantRedisPrefix)(tenantId);
20
+ return {
21
+ base,
22
+ rateLimit: (_b = overrides.rateLimit) !== null && _b !== void 0 ? _b : `${base}rate:`,
23
+ serviceTokenRateLimit: (_c = overrides.serviceTokenRateLimit) !== null && _c !== void 0 ? _c : `${base}service-token-rate:`,
24
+ sessionRevocation: (_d = overrides.sessionRevocation) !== null && _d !== void 0 ? _d : `${base}session:`,
25
+ accessTokenRevocation: (_e = overrides.accessTokenRevocation) !== null && _e !== void 0 ? _e : `${base}jti:`,
26
+ refreshTokenRotation: (_f = overrides.refreshTokenRotation) !== null && _f !== void 0 ? _f : `${base}refresh:`,
27
+ refreshTokenFamily: (_g = overrides.refreshTokenFamily) !== null && _g !== void 0 ? _g : `${base}refresh-family:`,
28
+ consoleEntry: (_h = overrides.consoleEntry) !== null && _h !== void 0 ? _h : `${base}console-entry:`,
29
+ };
30
+ }
31
+ /**
32
+ * Crea tutti gli store Redis di un tenant con prefissi gia isolati.
33
+ *
34
+ * `applyTo` restituisce una copia delle opzioni con gli store collegati:
35
+ * `publicAuthRateLimit`, `serviceTokens`, `sessionRevocation`,
36
+ * `accessTokenRevocation`, `refreshTokenRotation` e `consoleEntry`.
37
+ */
38
+ function createAccessiTenantStores(options) {
39
+ var _a;
40
+ if (!(options === null || options === void 0 ? void 0 : options.tenantId) || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(options.tenantId)) {
41
+ throw new Error('createAccessiTenantStores: tenantId non valido.');
42
+ }
43
+ if (!options.client || typeof options.client.eval !== 'function') {
44
+ throw new Error('createAccessiTenantStores: client.eval non configurato.');
45
+ }
46
+ const encryptionKey = options.encryptionKey;
47
+ if (encryptionKey === undefined || encryptionKey === null || encryptionKey.length === 0) {
48
+ throw new Error('createAccessiTenantStores: encryptionKey obbligatoria per i ticket console.');
49
+ }
50
+ const prefixes = getAccessiTenantStorePrefixes(options.tenantId, Object.assign(Object.assign({}, (options.prefix ? { base: options.prefix } : {})), ((_a = options.prefixes) !== null && _a !== void 0 ? _a : {})));
51
+ const { client } = options;
52
+ const enableRefreshTokenRotation = options.enableRefreshTokenRotation !== false;
53
+ const rateLimit = (0, rateLimitStore_1.createRedisRateLimitStore)({ client, keyPrefix: prefixes.rateLimit });
54
+ const serviceTokenRateLimit = (0, rateLimitStore_1.createRedisRateLimitStore)({
55
+ client,
56
+ keyPrefix: prefixes.serviceTokenRateLimit,
57
+ });
58
+ const sessionRevocation = (0, sessionRevocation_1.createRedisSessionRevocationStore)(Object.assign({ client, keyPrefix: prefixes.sessionRevocation }, (options.sessionRevocationTtlMs !== undefined ? { ttlMs: options.sessionRevocationTtlMs } : {})));
59
+ const accessTokenRevocation = (0, accessTokenRevocation_1.createRedisAccessTokenRevocationStore)(Object.assign({ client, keyPrefix: prefixes.accessTokenRevocation }, (options.accessTokenRevocationTtlMs !== undefined ? { ttlMs: options.accessTokenRevocationTtlMs } : {})));
60
+ const refreshTokenRotation = (0, refreshTokenRotation_1.createRedisRefreshTokenRotationStore)({
61
+ client,
62
+ keyPrefix: prefixes.refreshTokenRotation,
63
+ familyKeyPrefix: prefixes.refreshTokenFamily,
64
+ });
65
+ const consoleEntry = (0, consoleEntry_1.createRedisConsoleEntryStore)(Object.assign({ client,
66
+ encryptionKey, keyPrefix: prefixes.consoleEntry }, (options.consoleEntryMaxIssueAttempts !== undefined
67
+ ? { maxIssueAttempts: options.consoleEntryMaxIssueAttempts }
68
+ : {})));
69
+ return {
70
+ tenantId: options.tenantId,
71
+ prefixes,
72
+ rateLimit,
73
+ serviceTokenRateLimit,
74
+ sessionRevocation,
75
+ accessTokenRevocation,
76
+ refreshTokenRotation,
77
+ consoleEntry,
78
+ applyTo(accessiOptions) {
79
+ var _a, _b;
80
+ return Object.assign(Object.assign({}, accessiOptions), { tenantId: options.tenantId, publicAuthRateLimit: Object.assign(Object.assign({}, accessiOptions.publicAuthRateLimit), { store: rateLimit }), serviceTokens: Object.assign(Object.assign({}, accessiOptions.serviceTokens), { rateLimitStore: serviceTokenRateLimit }), sessionRevocation: Object.assign(Object.assign({}, accessiOptions.sessionRevocation), { store: sessionRevocation }), accessTokenRevocation: Object.assign(Object.assign({}, accessiOptions.accessTokenRevocation), { store: accessTokenRevocation }), refreshTokenRotation: Object.assign(Object.assign({}, accessiOptions.refreshTokenRotation), { enabled: (_b = (_a = accessiOptions.refreshTokenRotation) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : enableRefreshTokenRotation, store: refreshTokenRotation }), consoleEntry: Object.assign(Object.assign({}, accessiOptions.consoleEntry), { store: consoleEntry }) });
81
+ },
82
+ };
83
+ }
84
+ //# sourceMappingURL=tenantStores.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenantStores.js","sourceRoot":"","sources":["../../../src/accessi/server/tenantStores.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccessiOptions } from '../../accessi-module/AccessiModule';\nimport { getAccessiTenantRedisPrefix } from './tenants';\nimport { createRedisRateLimitStore } from '../../accessi-module/security/rateLimitStore';\nimport type { RateLimitStore } from '../../accessi-module/security/rateLimitStore';\nimport { createRedisConsoleEntryStore } from '../../accessi-module/security/consoleEntry';\nimport type { ConsoleEntryStore } from '../../accessi-module/security/consoleEntry';\nimport { createRedisSessionRevocationStore } from '../../accessi-module/security/sessionRevocation';\nimport type { SessionRevocationStore } from '../../accessi-module/security/sessionRevocation';\nimport { createRedisAccessTokenRevocationStore } from '../../accessi-module/security/accessTokenRevocation';\nimport type { AccessTokenRevocationStore } from '../../accessi-module/security/accessTokenRevocation';\nimport { createRedisRefreshTokenRotationStore } from '../../accessi-module/security/refreshTokenRotation';\nimport type { RefreshTokenRotationStore } from '../../accessi-module/security/refreshTokenRotation';\n\n/** Client Redis minimo accettato dagli store Accessi. */\nexport interface AccessiTenantRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\n/** Prefissi Redis per singolo store, tutti relativi al tenant. */\nexport interface AccessiTenantStorePrefixes {\n base?: string;\n rateLimit?: string;\n serviceTokenRateLimit?: string;\n sessionRevocation?: string;\n accessTokenRevocation?: string;\n refreshTokenRotation?: string;\n refreshTokenFamily?: string;\n consoleEntry?: string;\n}\n\nexport interface ResolvedAccessiTenantStorePrefixes {\n base: string;\n rateLimit: string;\n serviceTokenRateLimit: string;\n sessionRevocation: string;\n accessTokenRevocation: string;\n refreshTokenRotation: string;\n refreshTokenFamily: string;\n consoleEntry: string;\n}\n\nexport interface CreateAccessiTenantStoresOptions {\n tenantId: string;\n client: AccessiTenantRedisClient;\n /** Segreto per cifrare i ticket console a riposo. Usare un segreto del tenant. */\n encryptionKey: string | Buffer;\n /** Base prefix. Default: `accessi:<tenantId>:`. */\n prefix?: string;\n prefixes?: AccessiTenantStorePrefixes;\n /** TTL revoca sessioni. Default dello store (365 giorni). */\n sessionRevocationTtlMs?: number;\n /** TTL revoca jti. Default dello store (24 ore). */\n accessTokenRevocationTtlMs?: number;\n consoleEntryMaxIssueAttempts?: number;\n /** Attiva la rotazione refresh token nell'options risultante. Default: true. */\n enableRefreshTokenRotation?: boolean;\n}\n\nexport interface AccessiTenantStores {\n tenantId: string;\n prefixes: ResolvedAccessiTenantStorePrefixes;\n rateLimit: RateLimitStore;\n serviceTokenRateLimit: RateLimitStore;\n sessionRevocation: SessionRevocationStore;\n accessTokenRevocation: AccessTokenRevocationStore;\n refreshTokenRotation: RefreshTokenRotationStore;\n consoleEntry: ConsoleEntryStore;\n /** Collega tutti gli store alle opzioni Accessi del tenant. */\n applyTo(options: AccessiOptions): AccessiOptions;\n}\n\n/**\n * Prefissi Redis isolati per tenant.\n *\n * Ogni store ha un namespace distinto: condividere un prefisso tra tenant\n * significherebbe condividere rate limit, revoche o refresh token.\n */\nexport function getAccessiTenantStorePrefixes(\n tenantId: string,\n overrides: AccessiTenantStorePrefixes = {},\n): ResolvedAccessiTenantStorePrefixes {\n const base = overrides.base ?? getAccessiTenantRedisPrefix(tenantId);\n return {\n base,\n rateLimit: overrides.rateLimit ?? `${base}rate:`,\n serviceTokenRateLimit: overrides.serviceTokenRateLimit ?? `${base}service-token-rate:`,\n sessionRevocation: overrides.sessionRevocation ?? `${base}session:`,\n accessTokenRevocation: overrides.accessTokenRevocation ?? `${base}jti:`,\n refreshTokenRotation: overrides.refreshTokenRotation ?? `${base}refresh:`,\n refreshTokenFamily: overrides.refreshTokenFamily ?? `${base}refresh-family:`,\n consoleEntry: overrides.consoleEntry ?? `${base}console-entry:`,\n };\n}\n\n/**\n * Crea tutti gli store Redis di un tenant con prefissi gia isolati.\n *\n * `applyTo` restituisce una copia delle opzioni con gli store collegati:\n * `publicAuthRateLimit`, `serviceTokens`, `sessionRevocation`,\n * `accessTokenRevocation`, `refreshTokenRotation` e `consoleEntry`.\n */\nexport function createAccessiTenantStores(\n options: CreateAccessiTenantStoresOptions,\n): AccessiTenantStores {\n if (!options?.tenantId || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(options.tenantId)) {\n throw new Error('createAccessiTenantStores: tenantId non valido.');\n }\n if (!options.client || typeof options.client.eval !== 'function') {\n throw new Error('createAccessiTenantStores: client.eval non configurato.');\n }\n const encryptionKey = options.encryptionKey;\n if (encryptionKey === undefined || encryptionKey === null || encryptionKey.length === 0) {\n throw new Error('createAccessiTenantStores: encryptionKey obbligatoria per i ticket console.');\n }\n\n const prefixes = getAccessiTenantStorePrefixes(options.tenantId, {\n ...(options.prefix ? { base: options.prefix } : {}),\n ...(options.prefixes ?? {}),\n });\n const { client } = options;\n const enableRefreshTokenRotation = options.enableRefreshTokenRotation !== false;\n\n const rateLimit = createRedisRateLimitStore({ client, keyPrefix: prefixes.rateLimit });\n const serviceTokenRateLimit = createRedisRateLimitStore({\n client,\n keyPrefix: prefixes.serviceTokenRateLimit,\n });\n const sessionRevocation = createRedisSessionRevocationStore({\n client,\n keyPrefix: prefixes.sessionRevocation,\n ...(options.sessionRevocationTtlMs !== undefined ? { ttlMs: options.sessionRevocationTtlMs } : {}),\n });\n const accessTokenRevocation = createRedisAccessTokenRevocationStore({\n client,\n keyPrefix: prefixes.accessTokenRevocation,\n ...(options.accessTokenRevocationTtlMs !== undefined ? { ttlMs: options.accessTokenRevocationTtlMs } : {}),\n });\n const refreshTokenRotation = createRedisRefreshTokenRotationStore({\n client,\n keyPrefix: prefixes.refreshTokenRotation,\n familyKeyPrefix: prefixes.refreshTokenFamily,\n });\n const consoleEntry = createRedisConsoleEntryStore({\n client,\n encryptionKey,\n keyPrefix: prefixes.consoleEntry,\n ...(options.consoleEntryMaxIssueAttempts !== undefined\n ? { maxIssueAttempts: options.consoleEntryMaxIssueAttempts }\n : {}),\n });\n\n return {\n tenantId: options.tenantId,\n prefixes,\n rateLimit,\n serviceTokenRateLimit,\n sessionRevocation,\n accessTokenRevocation,\n refreshTokenRotation,\n consoleEntry,\n applyTo(accessiOptions) {\n return {\n ...accessiOptions,\n tenantId: options.tenantId,\n publicAuthRateLimit: {\n ...accessiOptions.publicAuthRateLimit,\n store: rateLimit,\n },\n serviceTokens: {\n ...accessiOptions.serviceTokens,\n rateLimitStore: serviceTokenRateLimit,\n },\n sessionRevocation: {\n ...accessiOptions.sessionRevocation,\n store: sessionRevocation,\n },\n accessTokenRevocation: {\n ...accessiOptions.accessTokenRevocation,\n store: accessTokenRevocation,\n },\n refreshTokenRotation: {\n ...accessiOptions.refreshTokenRotation,\n enabled: accessiOptions.refreshTokenRotation?.enabled ?? enableRefreshTokenRotation,\n store: refreshTokenRotation,\n },\n consoleEntry: {\n ...accessiOptions.consoleEntry,\n store: consoleEntry,\n },\n };\n },\n };\n}\n"]}
@@ -1,15 +1,80 @@
1
- import type { AccessiOptions } from '../../accessi-module/AccessiModule';
2
- /** Variabili usate dal manifest del tenant (oltre alle normali `ACCESSI_*`). */
3
- export interface AccessiTenantEnv extends NodeJS.ProcessEnv {
4
- ACCESSI_TENANT_ID?: string;
5
- /** Host separati da virgola; supporta `*.example.com`. */
6
- ACCESSI_TENANT_HOSTS?: string;
7
- /** Prefisso path, es. `/tenant-a`. */
8
- ACCESSI_TENANT_PATH?: string;
9
- ACCESSI_TENANT_ENABLED?: string;
10
- /** Porta del servizio quando si usa `startAccessiTenants`. */
11
- ACCESSI_TENANT_PORT?: string;
12
- ACCESSI_TENANT_DESCRIPTION?: string;
1
+ import type { AccessiOptions, AccessiHttpOptions, AdminBootstrapOptions, CatalogScriptsOptions, ConsoleEntryOptions, EmailOptions, ExtensionFieldsOptions, FederatedAuthenticationOptions, JwtOptions, PublicAuthRateLimitOptions, PublicRegistrationOptions, ServiceTokenOptions, SystemSeedsOptions } from '../../accessi-module/AccessiModule';
2
+ import type { AccessiEdgeRateLimitOptions } from '../../accessi-module/security/edgeRateLimit';
3
+ import type { AccessiAuthCookieOptions } from '../../accessi-module/security/authCookies';
4
+ import type { AccessiMetricsOptions } from '../../accessi-module/observability/metrics';
5
+ import type { AccessiAuditOptions } from '../../accessi-module/security/audit';
6
+ import type { SessionRevocationOptions } from '../../accessi-module/security/sessionRevocation';
7
+ import type { AccessTokenRevocationOptions } from '../../accessi-module/security/accessTokenRevocation';
8
+ import type { RefreshTokenRotationOptions } from '../../accessi-module/security/refreshTokenRotation';
9
+ import type { OpenApiSetupOptions } from '../../openapi/OpenApiConfig';
10
+ /** Identita e routing del tenant. */
11
+ export interface AccessiTenantSettings {
12
+ /** Minuscole, numeri, `-` e `_`; max 63 caratteri. */
13
+ id: string;
14
+ /** Host esatti o wildcard `*.example.com`. */
15
+ hosts?: string[];
16
+ /** Prefisso path, es. `/acme`. */
17
+ path?: string;
18
+ /** Porta del processo quando si usa il supervisore. */
19
+ port?: number;
20
+ /** Default `true`. */
21
+ enabled?: boolean;
22
+ description?: string;
23
+ }
24
+ /** Connessione Firebird del tenant, tipizzata senza callback. */
25
+ export interface AccessiTenantDatabaseOptions {
26
+ host: string;
27
+ port?: number;
28
+ database: string;
29
+ user?: string;
30
+ password?: string;
31
+ /** `disabled`/`optional`; `required` e il default del driver. */
32
+ wireCrypt?: 'required' | 'optional' | 'disabled';
33
+ pluginName?: string;
34
+ }
35
+ /**
36
+ * Configurazione serializzabile di un tenant.
37
+ *
38
+ * I campi che contengono store, callback o logger (`store`, `sink`, `match`,
39
+ * `onDocument`, `logger`, ...) sono esclusi: vanno collegati dal backend
40
+ * ospitante, non da un file di configurazione.
41
+ */
42
+ export interface AccessiTenantEnvironment {
43
+ tenant: AccessiTenantSettings;
44
+ database: AccessiTenantDatabaseOptions;
45
+ jwt: JwtOptions;
46
+ email: EmailOptions;
47
+ encryptionKey: string;
48
+ confirmationEmailUrl: string;
49
+ confirmationEmailReturnUrl: string;
50
+ confirmationEmailPrefix?: string;
51
+ customResetPage?: string;
52
+ publicBaseUrl?: string;
53
+ consoleApiBaseUrl?: string;
54
+ mockDemoUser?: boolean;
55
+ passwordExpiration?: boolean;
56
+ passwordExpirationDays?: number;
57
+ legacyPasswordMigrationOnStartup?: boolean;
58
+ autoUpdateDatabase?: boolean;
59
+ http?: AccessiHttpOptions;
60
+ authCookie?: AccessiAuthCookieOptions;
61
+ publicAuthRateLimit?: Omit<PublicAuthRateLimitOptions, 'store'>;
62
+ publicRegistration?: PublicRegistrationOptions;
63
+ federatedAuthentication?: FederatedAuthenticationOptions;
64
+ extensionFieldsOptions?: ExtensionFieldsOptions[];
65
+ serviceTokens?: Omit<ServiceTokenOptions, 'rateLimitStore'>;
66
+ catalogScripts?: CatalogScriptsOptions;
67
+ systemSeeds?: SystemSeedsOptions;
68
+ adminBootstrap?: AdminBootstrapOptions;
69
+ openapi?: Omit<OpenApiSetupOptions, 'onDocument'>;
70
+ consoleEntry?: Omit<ConsoleEntryOptions, 'store'>;
71
+ edgeRateLimit?: Omit<AccessiEdgeRateLimitOptions, 'store' | 'match'>;
72
+ sessionRevocation?: Omit<SessionRevocationOptions, 'store'>;
73
+ accessTokenRevocation?: Omit<AccessTokenRevocationOptions, 'store'>;
74
+ refreshTokenRotation?: Omit<RefreshTokenRotationOptions, 'store'>;
75
+ metrics?: Omit<AccessiMetricsOptions, 'sink'>;
76
+ audit?: Omit<AccessiAuditOptions, 'sinks' | 'store' | 'tenantId'>;
77
+ bufferLogs?: boolean;
13
78
  }
14
79
  export interface AccessiTenant {
15
80
  id: string;
@@ -18,10 +83,9 @@ export interface AccessiTenant {
18
83
  enabled: boolean;
19
84
  description?: string;
20
85
  port?: number;
21
- /** Nome del file `.env` di origine (mai il contenuto). */
86
+ /** Nome del file `*.environment.ts` di origine (mai il contenuto). */
22
87
  file: string;
23
- /** Variabili risolte del tenant (base + file); non serializzarle nei log. */
24
- env: NodeJS.ProcessEnv;
88
+ environmentFile: string;
25
89
  options: AccessiOptions;
26
90
  }
27
91
  export interface AccessiTenantSummary {
@@ -63,10 +127,8 @@ export interface AccessiTenantRegistry {
63
127
  summary(): AccessiTenantSummary[];
64
128
  }
65
129
  export interface LoadAccessiTenantsOptions {
66
- /** Cartella con `<tenant>.env`. */
130
+ /** Cartella con `<id>.environment.ts`. */
67
131
  directory: string;
68
- /** Variabili base (default `process.env`); il file tenant vince. */
69
- env?: NodeJS.ProcessEnv;
70
132
  /** Abilita la risoluzione via header `x-accessi-tenant`. Default: false. */
71
133
  allowHeader?: boolean;
72
134
  /** Consente issuer/audience uguali tra tenant (sconsigliato). Default: false. */
@@ -78,20 +140,47 @@ export declare class AccessiTenantConfigurationError extends Error {
78
140
  constructor(issues: string[]);
79
141
  }
80
142
  /**
81
- * Parser minimale di un file `.env`.
143
+ * Helper di tipizzazione per i file tenant.
144
+ *
145
+ * ```ts
146
+ * import type { AccessiTenantEnvironment } from 'emilsoftware-utilities/accessi/server';
147
+ * import { defineAccessiTenantEnvironment } from 'emilsoftware-utilities/accessi/server';
82
148
  *
83
- * Supporta commenti, `export KEY=...`, valori tra apici singoli o doppi e le
84
- * sequenze `\n`, `\r`, `\t`, `\"` e `\\` nei valori tra doppi apici. Non
85
- * interpreta variabili annidate: la configurazione tenant deve restare esplicita.
149
+ * export default defineAccessiTenantEnvironment({
150
+ * tenant: { id: 'acme', hosts: ['accessi.acme.example.com'] },
151
+ * database: { host: 'db-acme', database: '/data/acme.fdb' },
152
+ * jwt: { secret: process.env.ACME_JWT_SECRET!, expiresIn: '1h' },
153
+ * email: { ... },
154
+ * encryptionKey: process.env.ACME_ENCRYPTION_KEY!,
155
+ * confirmationEmailUrl: 'https://accessi.acme.example.com',
156
+ * confirmationEmailReturnUrl: 'https://accessi.acme.example.com',
157
+ * });
158
+ * ```
86
159
  */
87
- export declare function parseAccessiTenantEnvFile(content: string): Record<string, string>;
160
+ export declare function defineAccessiTenantEnvironment<const T extends AccessiTenantEnvironment>(environment: T): T;
88
161
  /** Prefisso Redis consigliato per isolare gli store del tenant. */
89
162
  export declare function getAccessiTenantRedisPrefix(tenantId: string): string;
90
163
  /**
91
- * Carica i tenant da una cartella di file `<id>.env`.
164
+ * Converte la configurazione tipizzata in `AccessiOptions`.
165
+ *
166
+ * Applica i default di Firebird e collega `tenantId`; non crea store o sink,
167
+ * che restano responsabilita del backend ospitante.
168
+ */
169
+ export declare function accessiOptionsFromTenantEnvironment(environment: AccessiTenantEnvironment): AccessiOptions;
170
+ /**
171
+ * Carica un singolo file `*.environment.ts`.
172
+ *
173
+ * Usa jiti per eseguire TypeScript anche nel pacchetto compilato, senza
174
+ * richiedere un passo di build separato per i file tenant.
175
+ */
176
+ export declare function loadAccessiTenantEnvironmentFile(file: string): {
177
+ tenant: AccessiTenant;
178
+ environment: AccessiTenantEnvironment;
179
+ };
180
+ /**
181
+ * Carica i tenant da una cartella di file `<id>.environment.ts`.
92
182
  *
93
- * Non modifica `process.env`: ogni tenant riceve una copia isolata delle
94
- * variabili. Un errore in un file blocca l'intero caricamento, cosi non si
95
- * avvia un tenant con configurazione parziale.
183
+ * La validazione e aggregata: un errore blocca l'intera cartella, cosi non si
184
+ * avvia un tenant con configurazione parziale. Nessun segreto viene stampato.
96
185
  */
97
186
  export declare function loadAccessiTenants(options: LoadAccessiTenantsOptions): AccessiTenantRegistry;