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.
- package/deploy/README.md +45 -0
- package/deploy/docker/docker-compose.yml +51 -0
- package/deploy/nginx/accessi-tenants.conf +41 -0
- package/deploy/systemd/accessi-tenant@.service +46 -0
- package/deploy/systemd/tenant.secrets.example +11 -0
- package/deploy/traefik/dynamic.yml +57 -0
- package/dist/accessi/server/index.d.ts +1 -0
- package/dist/accessi/server/index.js +1 -0
- package/dist/accessi/server/index.js.map +1 -1
- package/dist/accessi/server/tenantStores.d.ts +74 -0
- package/dist/accessi/server/tenantStores.js +84 -0
- package/dist/accessi/server/tenantStores.js.map +1 -0
- package/dist/accessi/server/tenants.d.ts +116 -27
- package/dist/accessi/server/tenants.js +186 -104
- package/dist/accessi/server/tenants.js.map +1 -1
- package/dist/accessi-module/Console/console.js +2 -2
- package/dist/accessi-module/Console/wiki.js +1 -1
- package/dist/accessi-module/Console/wiki.js.map +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.82.tgz +0 -0
- package/dist/scripts/startAccessiTenantService.d.ts +1 -0
- package/dist/scripts/startAccessiTenantService.js +35 -0
- package/dist/scripts/startAccessiTenantService.js.map +1 -0
- package/docs/adr/0002-multi-tenancy.md +63 -35
- package/docs/multi-tenancy.md +139 -103
- package/package.json +7 -4
- package/dist/emilsoftware-utilities-2.0.0-dev.80.tgz +0 -0
package/deploy/README.md
ADDED
|
@@ -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;
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
86
|
+
/** Nome del file `*.environment.ts` di origine (mai il contenuto). */
|
|
22
87
|
file: string;
|
|
23
|
-
|
|
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 `<
|
|
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
|
-
*
|
|
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
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
94
|
-
*
|
|
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;
|