@3sixty/api-tenant-processor 0.0.0-stage → 0.20.0-next.1
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/LICENSE +201 -0
- package/README.md +24 -2
- package/dist/es/entities/tenant.js +77 -0
- package/dist/es/entities/tenant.js.map +1 -0
- package/dist/es/entities/tenantV0.js +61 -0
- package/dist/es/entities/tenantV0.js.map +1 -0
- package/dist/es/entities/tenantV1.js +77 -0
- package/dist/es/entities/tenantV1.js.map +1 -0
- package/dist/es/index.js +30 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/models/ISingleTenantProcessorConfig.js +4 -0
- package/dist/es/models/ISingleTenantProcessorConfig.js.map +1 -0
- package/dist/es/models/ISingleTenantProcessorConstructorOptions.js +2 -0
- package/dist/es/models/ISingleTenantProcessorConstructorOptions.js.map +1 -0
- package/dist/es/models/ITenantAdminServiceConfig.js +4 -0
- package/dist/es/models/ITenantAdminServiceConfig.js.map +1 -0
- package/dist/es/models/ITenantAdminServiceConstructorOptions.js +2 -0
- package/dist/es/models/ITenantAdminServiceConstructorOptions.js.map +1 -0
- package/dist/es/models/ITenantProcessorConfig.js +4 -0
- package/dist/es/models/ITenantProcessorConfig.js.map +1 -0
- package/dist/es/models/ITenantProcessorConstructorOptions.js +2 -0
- package/dist/es/models/ITenantProcessorConstructorOptions.js.map +1 -0
- package/dist/es/models/api/ITenantCreateRequest.js +2 -0
- package/dist/es/models/api/ITenantCreateRequest.js.map +1 -0
- package/dist/es/models/api/ITenantGetByApiKeyRequest.js +4 -0
- package/dist/es/models/api/ITenantGetByApiKeyRequest.js.map +1 -0
- package/dist/es/models/api/ITenantGetByIdRequest.js +4 -0
- package/dist/es/models/api/ITenantGetByIdRequest.js.map +1 -0
- package/dist/es/models/api/ITenantGetByPublicOriginRequest.js +4 -0
- package/dist/es/models/api/ITenantGetByPublicOriginRequest.js.map +1 -0
- package/dist/es/models/api/ITenantGetResponse.js +2 -0
- package/dist/es/models/api/ITenantGetResponse.js.map +1 -0
- package/dist/es/models/api/ITenantListRequest.js +4 -0
- package/dist/es/models/api/ITenantListRequest.js.map +1 -0
- package/dist/es/models/api/ITenantListResponse.js +2 -0
- package/dist/es/models/api/ITenantListResponse.js.map +1 -0
- package/dist/es/models/api/ITenantRemoveRequest.js +4 -0
- package/dist/es/models/api/ITenantRemoveRequest.js.map +1 -0
- package/dist/es/models/api/ITenantUpdateRequest.js +2 -0
- package/dist/es/models/api/ITenantUpdateRequest.js.map +1 -0
- package/dist/es/restEntryPoints.js +13 -0
- package/dist/es/restEntryPoints.js.map +1 -0
- package/dist/es/schema.js +64 -0
- package/dist/es/schema.js.map +1 -0
- package/dist/es/singleTenantProcessor.js +81 -0
- package/dist/es/singleTenantProcessor.js.map +1 -0
- package/dist/es/tenantAdminService.js +510 -0
- package/dist/es/tenantAdminService.js.map +1 -0
- package/dist/es/tenantIdContextIdHandler.js +47 -0
- package/dist/es/tenantIdContextIdHandler.js.map +1 -0
- package/dist/es/tenantProcessor.js +164 -0
- package/dist/es/tenantProcessor.js.map +1 -0
- package/dist/es/tenantRoutes.js +417 -0
- package/dist/es/tenantRoutes.js.map +1 -0
- package/dist/es/utils/tenantIdHelper.js +23 -0
- package/dist/es/utils/tenantIdHelper.js.map +1 -0
- package/dist/types/entities/tenant.d.ts +37 -0
- package/dist/types/entities/tenantV0.d.ts +29 -0
- package/dist/types/entities/tenantV1.d.ts +37 -0
- package/dist/types/index.d.ts +26 -0
- package/dist/types/models/ISingleTenantProcessorConfig.d.ts +9 -0
- package/dist/types/models/ISingleTenantProcessorConstructorOptions.d.ts +10 -0
- package/dist/types/models/ITenantAdminServiceConfig.d.ts +24 -0
- package/dist/types/models/ITenantAdminServiceConstructorOptions.d.ts +20 -0
- package/dist/types/models/ITenantProcessorConfig.d.ts +18 -0
- package/dist/types/models/ITenantProcessorConstructorOptions.d.ts +15 -0
- package/dist/types/models/api/ITenantCreateRequest.d.ts +12 -0
- package/dist/types/models/api/ITenantGetByApiKeyRequest.d.ts +14 -0
- package/dist/types/models/api/ITenantGetByIdRequest.d.ts +14 -0
- package/dist/types/models/api/ITenantGetByPublicOriginRequest.d.ts +14 -0
- package/dist/types/models/api/ITenantGetResponse.d.ts +10 -0
- package/dist/types/models/api/ITenantListRequest.d.ts +27 -0
- package/dist/types/models/api/ITenantListResponse.d.ts +17 -0
- package/dist/types/models/api/ITenantRemoveRequest.d.ts +14 -0
- package/dist/types/models/api/ITenantUpdateRequest.d.ts +19 -0
- package/dist/types/restEntryPoints.d.ts +5 -0
- package/dist/types/schema.d.ts +5 -0
- package/dist/types/singleTenantProcessor.d.ts +41 -0
- package/dist/types/tenantAdminService.d.ts +109 -0
- package/dist/types/tenantIdContextIdHandler.d.ts +33 -0
- package/dist/types/tenantProcessor.d.ts +34 -0
- package/dist/types/tenantRoutes.d.ts +78 -0
- package/dist/types/utils/tenantIdHelper.d.ts +15 -0
- package/docs/changelog.md +17 -0
- package/docs/examples.md +81 -0
- package/docs/reference/classes/SingleTenantProcessor.md +130 -0
- package/docs/reference/classes/Tenant.md +77 -0
- package/docs/reference/classes/TenantAdminService.md +393 -0
- package/docs/reference/classes/TenantIdContextIdHandler.md +123 -0
- package/docs/reference/classes/TenantIdHelper.md +41 -0
- package/docs/reference/classes/TenantProcessor.md +103 -0
- package/docs/reference/classes/TenantV0.md +61 -0
- package/docs/reference/classes/TenantV1.md +77 -0
- package/docs/reference/functions/generateRestRoutesTenants.md +25 -0
- package/docs/reference/functions/initSchema.md +10 -0
- package/docs/reference/functions/tenantByApiKey.md +31 -0
- package/docs/reference/functions/tenantById.md +31 -0
- package/docs/reference/functions/tenantByPublicOrigin.md +31 -0
- package/docs/reference/functions/tenantCreate.md +37 -0
- package/docs/reference/functions/tenantList.md +31 -0
- package/docs/reference/functions/tenantRemove.md +31 -0
- package/docs/reference/functions/tenantUpdate.md +31 -0
- package/docs/reference/index.md +47 -0
- package/docs/reference/interfaces/ISingleTenantProcessorConfig.md +11 -0
- package/docs/reference/interfaces/ISingleTenantProcessorConstructorOptions.md +11 -0
- package/docs/reference/interfaces/ITenantAdminServiceConfig.md +49 -0
- package/docs/reference/interfaces/ITenantAdminServiceConstructorOptions.md +39 -0
- package/docs/reference/interfaces/ITenantCreateRequest.md +17 -0
- package/docs/reference/interfaces/ITenantGetByApiKeyRequest.md +17 -0
- package/docs/reference/interfaces/ITenantGetByIdRequest.md +17 -0
- package/docs/reference/interfaces/ITenantGetByPublicOriginRequest.md +17 -0
- package/docs/reference/interfaces/ITenantGetResponse.md +11 -0
- package/docs/reference/interfaces/ITenantListRequest.md +36 -0
- package/docs/reference/interfaces/ITenantListResponse.md +23 -0
- package/docs/reference/interfaces/ITenantProcessorConfig.md +33 -0
- package/docs/reference/interfaces/ITenantProcessorConstructorOptions.md +25 -0
- package/docs/reference/interfaces/ITenantRemoveRequest.md +17 -0
- package/docs/reference/interfaces/ITenantUpdateRequest.md +25 -0
- package/docs/reference/variables/restEntryPoints.md +5 -0
- package/docs/reference/variables/tagsTenants.md +5 -0
- package/locales/en.json +20 -0
- package/package.json +57 -5
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
// Copyright 2024 IOTA Stiftung.
|
|
2
|
+
// Copyright 2026 3Sixty Trade Ltd.
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
4
|
+
import { HealthCategory, HealthStatus, TenantEventType } from "@3sixty/api-models";
|
|
5
|
+
import { ContextIdKeys, ContextIdStore } from "@3sixty/context";
|
|
6
|
+
import { AlreadyExistsError, BaseError, ComponentFactory, Converter, GeneralError, Guards, Is, LfuCache, NotFoundError, Url } from "@3sixty/core";
|
|
7
|
+
import { Blake2b } from "@3sixty/crypto";
|
|
8
|
+
import { ComparisonOperator } from "@3sixty/entity";
|
|
9
|
+
import { EntityStorageConnectorFactory } from "@3sixty/entity-storage-models";
|
|
10
|
+
import { Tenant } from "./entities/tenant.js";
|
|
11
|
+
import { TenantIdHelper } from "./utils/tenantIdHelper.js";
|
|
12
|
+
/**
|
|
13
|
+
* Service for performing tenant administration operations.
|
|
14
|
+
*/
|
|
15
|
+
export class TenantAdminService {
|
|
16
|
+
/**
|
|
17
|
+
* Runtime name for the class.
|
|
18
|
+
*/
|
|
19
|
+
static CLASS_NAME = "TenantAdminService";
|
|
20
|
+
/**
|
|
21
|
+
* The default time to keep a tenant lookup in the cache.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
static _DEFAULT_CACHE_TTL_MS = 30000;
|
|
25
|
+
/**
|
|
26
|
+
* The cache key prefix for tenants looked up by id.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
static _CACHE_PREFIX_ID = "id:";
|
|
30
|
+
/**
|
|
31
|
+
* The cache key prefix for tenants looked up by api key.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
static _CACHE_PREFIX_API_KEY = "apiKey:";
|
|
35
|
+
/**
|
|
36
|
+
* The cache key prefix for tenants looked up by public origin.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
static _CACHE_PREFIX_PUBLIC_ORIGIN = "publicOrigin:";
|
|
40
|
+
/**
|
|
41
|
+
* The cache key prefix for tenants looked up by organization id, including legacy ids.
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
static _CACHE_PREFIX_ORGANIZATION_ID = "organizationId:";
|
|
45
|
+
/**
|
|
46
|
+
* Entity storage connector used by the service.
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
_entityStorageConnector;
|
|
50
|
+
/**
|
|
51
|
+
* Platform component used to fire tenant events.
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
_platformComponent;
|
|
55
|
+
/**
|
|
56
|
+
* The cache of tenant lookups, undefined when caching is disabled.
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
_tenantCache;
|
|
60
|
+
/**
|
|
61
|
+
* How long a cached tenant may live, counted from when it was read.
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
_tenantCacheTtlMs;
|
|
65
|
+
/**
|
|
66
|
+
* Create a new instance of TenantAdminService.
|
|
67
|
+
* @param options The options for the connector.
|
|
68
|
+
*/
|
|
69
|
+
constructor(options) {
|
|
70
|
+
this._entityStorageConnector = EntityStorageConnectorFactory.get(options?.tenantEntityStorageType ?? "tenant");
|
|
71
|
+
this._platformComponent = ComponentFactory.get(options?.platformComponentType ?? "platform");
|
|
72
|
+
this._tenantCacheTtlMs =
|
|
73
|
+
options?.config?.tenantCacheTtlMs ?? TenantAdminService._DEFAULT_CACHE_TTL_MS;
|
|
74
|
+
if (this._tenantCacheTtlMs > 0) {
|
|
75
|
+
this._tenantCache = new LfuCache({
|
|
76
|
+
capacity: options?.config?.tenantCacheCapacity,
|
|
77
|
+
ttiMs: this._tenantCacheTtlMs,
|
|
78
|
+
mutexTimeoutMs: options?.config?.tenantCacheMutexTimeoutMs
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Returns the class name of the component.
|
|
84
|
+
* @returns The class name of the component.
|
|
85
|
+
*/
|
|
86
|
+
className() {
|
|
87
|
+
return TenantAdminService.CLASS_NAME;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Stop the service and release the tenant cache.
|
|
91
|
+
* @returns A promise that resolves when the service has stopped.
|
|
92
|
+
*/
|
|
93
|
+
async stop() {
|
|
94
|
+
this._tenantCache?.destroy();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get a tenant by its id.
|
|
98
|
+
* @param tenantId The id of the tenant.
|
|
99
|
+
* @returns The tenant.
|
|
100
|
+
* @throws Error if the tenant is not found.
|
|
101
|
+
*/
|
|
102
|
+
async get(tenantId) {
|
|
103
|
+
Guards.stringHexLength(TenantAdminService.CLASS_NAME, "tenantId", tenantId, 32);
|
|
104
|
+
return this.cachedLookup(`${TenantAdminService._CACHE_PREFIX_ID}${tenantId}`, async () => {
|
|
105
|
+
const tenant = await this._entityStorageConnector.get(tenantId);
|
|
106
|
+
if (!Is.object(tenant)) {
|
|
107
|
+
throw new NotFoundError(TenantAdminService.CLASS_NAME, "tenantNotFound", tenantId);
|
|
108
|
+
}
|
|
109
|
+
return this.entityToModel(tenant);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get a tenant by its api key.
|
|
114
|
+
* @param apiKey The api key of the tenant.
|
|
115
|
+
* @returns The tenant.
|
|
116
|
+
* @throws Error if the tenant is not found.
|
|
117
|
+
*/
|
|
118
|
+
async getByApiKey(apiKey) {
|
|
119
|
+
Guards.stringHexLength(TenantAdminService.CLASS_NAME, "apiKey", apiKey, 32);
|
|
120
|
+
return this.cachedLookup(this.apiKeyCacheKey(apiKey), async () => {
|
|
121
|
+
const tenant = await this._entityStorageConnector.get(apiKey, "apiKey");
|
|
122
|
+
if (!Is.object(tenant)) {
|
|
123
|
+
throw new NotFoundError(TenantAdminService.CLASS_NAME, "tenantNotFound", apiKey);
|
|
124
|
+
}
|
|
125
|
+
return this.entityToModel(tenant);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get a tenant by its public origin.
|
|
130
|
+
* @param publicOrigin The origin of the tenant.
|
|
131
|
+
* @returns The tenant.
|
|
132
|
+
* @throws Error if the tenant is not found.
|
|
133
|
+
*/
|
|
134
|
+
async getByPublicOrigin(publicOrigin) {
|
|
135
|
+
Guards.stringValue(TenantAdminService.CLASS_NAME, "publicOrigin", publicOrigin);
|
|
136
|
+
return this.cachedLookup(`${TenantAdminService._CACHE_PREFIX_PUBLIC_ORIGIN}${publicOrigin}`, async () => {
|
|
137
|
+
const tenant = await this._entityStorageConnector.get(publicOrigin, "publicOrigin");
|
|
138
|
+
if (!Is.object(tenant)) {
|
|
139
|
+
throw new NotFoundError(TenantAdminService.CLASS_NAME, "tenantNotFound", publicOrigin);
|
|
140
|
+
}
|
|
141
|
+
return this.entityToModel(tenant);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get a tenant by its organization id, optionally searching legacy ids.
|
|
146
|
+
* @param organizationId The organization id of the tenant.
|
|
147
|
+
* @param includeLegacy Whether to also search the legacy organization id array.
|
|
148
|
+
* @returns The tenant.
|
|
149
|
+
* @throws Error if the tenant is not found.
|
|
150
|
+
*/
|
|
151
|
+
async getTenantByOrganizationId(organizationId, includeLegacy) {
|
|
152
|
+
Guards.stringValue(TenantAdminService.CLASS_NAME, "organizationId", organizationId);
|
|
153
|
+
return this.cachedLookup(this.organizationIdCacheKey(organizationId, includeLegacy ?? false), async () => {
|
|
154
|
+
let tenant = await this._entityStorageConnector.get(organizationId, "organizationId");
|
|
155
|
+
if (!Is.object(tenant) && (includeLegacy ?? false)) {
|
|
156
|
+
const result = await this._entityStorageConnector.query({
|
|
157
|
+
property: "organizationIdLegacy",
|
|
158
|
+
comparison: ComparisonOperator.Includes,
|
|
159
|
+
value: organizationId
|
|
160
|
+
}, undefined, undefined, undefined, 1);
|
|
161
|
+
tenant = result.entities[0];
|
|
162
|
+
}
|
|
163
|
+
if (!Is.object(tenant)) {
|
|
164
|
+
throw new NotFoundError(TenantAdminService.CLASS_NAME, "tenantNotFound", organizationId);
|
|
165
|
+
}
|
|
166
|
+
return this.entityToModel(tenant);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Create a tenant.
|
|
171
|
+
* @param tenant The tenant to store.
|
|
172
|
+
* @returns The tenant id.
|
|
173
|
+
*/
|
|
174
|
+
async create(tenant) {
|
|
175
|
+
Guards.objectValue(TenantAdminService.CLASS_NAME, "tenant", tenant);
|
|
176
|
+
Guards.stringValue(TenantAdminService.CLASS_NAME, "tenant.organizationId", tenant.organizationId);
|
|
177
|
+
if (Is.stringValue(tenant.id)) {
|
|
178
|
+
Guards.stringHexLength(TenantAdminService.CLASS_NAME, "tenant.id", tenant.id, 32);
|
|
179
|
+
const existing = await this._entityStorageConnector.get(tenant.id);
|
|
180
|
+
if (Is.object(existing)) {
|
|
181
|
+
throw new AlreadyExistsError(TenantAdminService.CLASS_NAME, "tenantAlreadyExists", tenant.id);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (Is.stringValue(tenant.apiKey)) {
|
|
185
|
+
Guards.stringHexLength(TenantAdminService.CLASS_NAME, "tenant.apiKey", tenant.apiKey, 32);
|
|
186
|
+
}
|
|
187
|
+
let publicOrigin;
|
|
188
|
+
if (Is.stringValue(tenant.publicOrigin)) {
|
|
189
|
+
Url.guard(TenantAdminService.CLASS_NAME, "tenant.publicOrigin", tenant.publicOrigin);
|
|
190
|
+
const url = new Url(tenant.publicOrigin);
|
|
191
|
+
const parts = url.parts();
|
|
192
|
+
publicOrigin = `${parts.schema}://${parts.host}${Is.integer(parts.port) ? `:${parts.port}` : ""}`;
|
|
193
|
+
}
|
|
194
|
+
if (Is.stringValue(tenant.apiKey)) {
|
|
195
|
+
const existingApiKey = await this._entityStorageConnector.get(tenant.apiKey, "apiKey");
|
|
196
|
+
if (Is.object(existingApiKey) && existingApiKey.id !== tenant.id) {
|
|
197
|
+
throw new GeneralError(TenantAdminService.CLASS_NAME, "apiKeyAlreadyInUse");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (Is.stringValue(publicOrigin)) {
|
|
201
|
+
const existingPublicOrigin = await this._entityStorageConnector.get(publicOrigin, "publicOrigin");
|
|
202
|
+
if (Is.object(existingPublicOrigin)) {
|
|
203
|
+
throw new AlreadyExistsError(TenantAdminService.CLASS_NAME, "publicOriginAlreadyExists", publicOrigin);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const existingOrgId = await this._entityStorageConnector.get(tenant.organizationId, "organizationId");
|
|
207
|
+
if (Is.object(existingOrgId)) {
|
|
208
|
+
throw new AlreadyExistsError(TenantAdminService.CLASS_NAME, "organizationIdAlreadyExists", tenant.organizationId);
|
|
209
|
+
}
|
|
210
|
+
const tenantEntity = {
|
|
211
|
+
id: tenant.id ?? TenantIdHelper.generateTenantId(),
|
|
212
|
+
apiKey: tenant.apiKey ?? TenantIdHelper.generateApiKey(),
|
|
213
|
+
dateCreated: new Date(Date.now()).toISOString(),
|
|
214
|
+
dateModified: new Date(Date.now()).toISOString(),
|
|
215
|
+
label: tenant.label,
|
|
216
|
+
publicOrigin,
|
|
217
|
+
organizationId: tenant.organizationId,
|
|
218
|
+
organizationIdLegacy: tenant.organizationIdLegacy
|
|
219
|
+
};
|
|
220
|
+
await this._entityStorageConnector.set(this.modelToEntity(tenantEntity));
|
|
221
|
+
// A new tenant cannot have a stale id, api key or public origin entry, because those are
|
|
222
|
+
// unique-checked above and failed lookups are never cached. Its organization ids can be
|
|
223
|
+
// stale though; only the primary organization index is checked, so an id that another
|
|
224
|
+
// tenant currently answers through its legacy list may already be cached against that
|
|
225
|
+
// tenant even though the new tenant now owns it outright.
|
|
226
|
+
this.invalidateTenant(tenantEntity);
|
|
227
|
+
await this._platformComponent.fireTenantEvent(tenantEntity.id, TenantEventType.Created);
|
|
228
|
+
return tenantEntity.id;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Update a tenant.
|
|
232
|
+
* @param tenant The tenant to update.
|
|
233
|
+
* @returns A promise that resolves when the tenant has been updated.
|
|
234
|
+
*/
|
|
235
|
+
async update(tenant) {
|
|
236
|
+
Guards.objectValue(TenantAdminService.CLASS_NAME, "tenant", tenant);
|
|
237
|
+
Guards.stringHexLength(TenantAdminService.CLASS_NAME, "tenant.id", tenant.id, 32);
|
|
238
|
+
Guards.stringValue(TenantAdminService.CLASS_NAME, "tenant.organizationId", tenant.organizationId);
|
|
239
|
+
if (Is.stringValue(tenant.apiKey)) {
|
|
240
|
+
Guards.stringHexLength(TenantAdminService.CLASS_NAME, "tenant.apiKey", tenant.apiKey, 32);
|
|
241
|
+
}
|
|
242
|
+
let publicOrigin;
|
|
243
|
+
if (Is.stringValue(tenant.publicOrigin)) {
|
|
244
|
+
Url.guard(TenantAdminService.CLASS_NAME, "tenant.publicOrigin", tenant.publicOrigin);
|
|
245
|
+
const url = new Url(tenant.publicOrigin);
|
|
246
|
+
const parts = url.parts();
|
|
247
|
+
publicOrigin = `${parts.schema}://${parts.host}${Is.integer(parts.port) ? `:${parts.port}` : ""}`;
|
|
248
|
+
}
|
|
249
|
+
const currentTenant = await this._entityStorageConnector.get(tenant.id);
|
|
250
|
+
if (!Is.object(currentTenant)) {
|
|
251
|
+
throw new NotFoundError(TenantAdminService.CLASS_NAME, "tenantNotFound", tenant.id);
|
|
252
|
+
}
|
|
253
|
+
if (Is.stringValue(tenant.apiKey)) {
|
|
254
|
+
const existingApiKey = await this._entityStorageConnector.get(tenant.apiKey, "apiKey");
|
|
255
|
+
if (Is.object(existingApiKey) && existingApiKey.id !== currentTenant.id) {
|
|
256
|
+
throw new GeneralError(TenantAdminService.CLASS_NAME, "apiKeyAlreadyInUse");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (Is.stringValue(publicOrigin)) {
|
|
260
|
+
const existingPublicOrigin = await this._entityStorageConnector.get(publicOrigin, "publicOrigin");
|
|
261
|
+
if (Is.object(existingPublicOrigin) && existingPublicOrigin.id !== currentTenant.id) {
|
|
262
|
+
throw new AlreadyExistsError(TenantAdminService.CLASS_NAME, "publicOriginAlreadyExists", publicOrigin);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const existingOrgId = await this._entityStorageConnector.get(tenant.organizationId, "organizationId");
|
|
266
|
+
if (Is.object(existingOrgId) && existingOrgId.id !== currentTenant.id) {
|
|
267
|
+
throw new AlreadyExistsError(TenantAdminService.CLASS_NAME, "organizationIdAlreadyExists", tenant.organizationId);
|
|
268
|
+
}
|
|
269
|
+
const currentTenantEntity = this.entityToModel(currentTenant);
|
|
270
|
+
const orgIdChanged = tenant.organizationId !== currentTenantEntity.organizationId;
|
|
271
|
+
let newOrganizationIdLegacy;
|
|
272
|
+
if (orgIdChanged) {
|
|
273
|
+
const legacySet = new Set(currentTenantEntity.organizationIdLegacy ?? []);
|
|
274
|
+
if (Is.stringValue(currentTenantEntity.organizationId)) {
|
|
275
|
+
legacySet.add(currentTenantEntity.organizationId);
|
|
276
|
+
}
|
|
277
|
+
legacySet.delete(tenant.organizationId);
|
|
278
|
+
newOrganizationIdLegacy = legacySet.size > 0 ? [...legacySet] : undefined;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
newOrganizationIdLegacy = Is.array(tenant.organizationIdLegacy)
|
|
282
|
+
? tenant.organizationIdLegacy
|
|
283
|
+
: currentTenantEntity.organizationIdLegacy;
|
|
284
|
+
}
|
|
285
|
+
const tenantEntity = {
|
|
286
|
+
id: tenant.id,
|
|
287
|
+
apiKey: tenant.apiKey ?? currentTenantEntity.apiKey,
|
|
288
|
+
dateCreated: currentTenantEntity.dateCreated,
|
|
289
|
+
dateModified: new Date(Date.now()).toISOString(),
|
|
290
|
+
label: tenant.label ?? currentTenantEntity.label,
|
|
291
|
+
publicOrigin: publicOrigin ?? currentTenantEntity.publicOrigin,
|
|
292
|
+
organizationId: tenant.organizationId,
|
|
293
|
+
organizationIdLegacy: newOrganizationIdLegacy
|
|
294
|
+
};
|
|
295
|
+
await this._entityStorageConnector.set(this.modelToEntity(tenantEntity));
|
|
296
|
+
// The api key, organization id and public origin can all have moved, so the entries
|
|
297
|
+
// cached under the previous values have to go as well as the new ones.
|
|
298
|
+
this.invalidateTenant(currentTenantEntity);
|
|
299
|
+
this.invalidateTenant(tenantEntity);
|
|
300
|
+
await this._platformComponent.fireTenantEvent(tenantEntity.id, TenantEventType.Updated);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Remove a tenant by its id.
|
|
304
|
+
* @param tenantId The id of the tenant.
|
|
305
|
+
* @returns Nothing.
|
|
306
|
+
*/
|
|
307
|
+
async remove(tenantId) {
|
|
308
|
+
Guards.stringHexLength(TenantAdminService.CLASS_NAME, "tenantId", tenantId, 32);
|
|
309
|
+
// The keys to drop have to be read before the tenant goes, but the drop itself has to happen
|
|
310
|
+
// after it, otherwise a lookup racing the delete would re-cache the tenant behind us.
|
|
311
|
+
const currentTenant = Is.empty(this._tenantCache)
|
|
312
|
+
? undefined
|
|
313
|
+
: await this._entityStorageConnector.get(tenantId);
|
|
314
|
+
await this._entityStorageConnector.remove(tenantId);
|
|
315
|
+
if (Is.object(currentTenant)) {
|
|
316
|
+
this.invalidateTenant(this.entityToModel(currentTenant));
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
// The tenant could not be read back, so its other keys are unknown, but an entry cached
|
|
320
|
+
// under the id before it went missing still has to go.
|
|
321
|
+
this._tenantCache?.delete(`${TenantAdminService._CACHE_PREFIX_ID}${tenantId}`);
|
|
322
|
+
}
|
|
323
|
+
await this._platformComponent.fireTenantEvent(tenantId, TenantEventType.Deleted);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Query tenants with pagination.
|
|
327
|
+
* @param conditions The conditions to filter the tenants.
|
|
328
|
+
* @param properties The properties to include in the returned tenants.
|
|
329
|
+
* @param cursor The cursor to start from.
|
|
330
|
+
* @param limit The maximum number of tenants to return.
|
|
331
|
+
* @returns The tenants and the next cursor if more tenants are available.
|
|
332
|
+
*/
|
|
333
|
+
async query(conditions, properties, cursor, limit) {
|
|
334
|
+
const result = await this._entityStorageConnector.query(conditions, undefined, properties, cursor, limit);
|
|
335
|
+
return {
|
|
336
|
+
tenants: result.entities.map(e => this.entityToModel(e)),
|
|
337
|
+
cursor: result.cursor
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Provision a temporary tenant for the application health cycle.
|
|
342
|
+
* @param contextIds Accumulated context IDs; receives the provisional tenant ID.
|
|
343
|
+
* @returns A promise that resolves when provisioning is complete.
|
|
344
|
+
*/
|
|
345
|
+
async healthApplicationInit(contextIds) {
|
|
346
|
+
const organizationId = contextIds[ContextIdKeys.Organization];
|
|
347
|
+
if (!Is.stringValue(organizationId)) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const tenantId = await this.create({
|
|
351
|
+
apiKey: TenantIdHelper.generateApiKey(),
|
|
352
|
+
organizationId,
|
|
353
|
+
label: "health-check"
|
|
354
|
+
});
|
|
355
|
+
contextIds[ContextIdKeys.Tenant] = tenantId;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Verify the provisioned tenant can be retrieved.
|
|
359
|
+
* The tenant ID is read from the active context set by the init pass.
|
|
360
|
+
* @param callback The callback to invoke when a deferred health result is ready.
|
|
361
|
+
* @returns The health entries for this component.
|
|
362
|
+
*/
|
|
363
|
+
async healthApplication(callback) {
|
|
364
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
365
|
+
const tenantId = contextIds?.[ContextIdKeys.Tenant];
|
|
366
|
+
if (!Is.stringValue(tenantId)) {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
let status = HealthStatus.Ok;
|
|
370
|
+
let healthError;
|
|
371
|
+
try {
|
|
372
|
+
await this.get(tenantId);
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
status = HealthStatus.Error;
|
|
376
|
+
healthError = BaseError.fromError(err);
|
|
377
|
+
}
|
|
378
|
+
return [
|
|
379
|
+
{
|
|
380
|
+
source: TenantAdminService.CLASS_NAME,
|
|
381
|
+
category: HealthCategory.Application,
|
|
382
|
+
status,
|
|
383
|
+
error: healthError
|
|
384
|
+
}
|
|
385
|
+
];
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Remove the temporary tenant provisioned during init.
|
|
389
|
+
* The tenant ID is read from the active context set by the init pass.
|
|
390
|
+
* Does nothing when no tenant ID is present.
|
|
391
|
+
* @returns A promise that resolves when teardown is complete.
|
|
392
|
+
*/
|
|
393
|
+
async healthApplicationTeardown() {
|
|
394
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
395
|
+
const tenantId = contextIds?.[ContextIdKeys.Tenant];
|
|
396
|
+
if (Is.stringValue(tenantId)) {
|
|
397
|
+
await this.remove(tenantId);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Build the cache key used for an api key lookup.
|
|
402
|
+
* The api key is hashed so neither the cache keys nor the mutex keys derived from them hold
|
|
403
|
+
* the credential itself.
|
|
404
|
+
* @param apiKey The api key being looked up.
|
|
405
|
+
* @returns The cache key.
|
|
406
|
+
* @internal
|
|
407
|
+
*/
|
|
408
|
+
apiKeyCacheKey(apiKey) {
|
|
409
|
+
const hashed = Converter.bytesToHex(Blake2b.sum256(Converter.utf8ToBytes(apiKey)));
|
|
410
|
+
return `${TenantAdminService._CACHE_PREFIX_API_KEY}${hashed}`;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Build the cache key used for an organization id lookup.
|
|
414
|
+
* @param organizationId The organization id being looked up.
|
|
415
|
+
* @param includeLegacy Whether the lookup also searches the legacy organization ids.
|
|
416
|
+
* @returns The cache key.
|
|
417
|
+
* @internal
|
|
418
|
+
*/
|
|
419
|
+
organizationIdCacheKey(organizationId, includeLegacy) {
|
|
420
|
+
return `${TenantAdminService._CACHE_PREFIX_ORGANIZATION_ID}${includeLegacy ? "legacy:" : ""}${organizationId}`;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Perform a lookup through the cache, falling back to a direct lookup when caching is disabled.
|
|
424
|
+
* Lookups that throw are not cached, so a missing tenant is re-checked against storage each time.
|
|
425
|
+
* @param cacheKey The key the tenant is cached under.
|
|
426
|
+
* @param lookup The lookup to perform when the tenant is not already cached.
|
|
427
|
+
* @returns The tenant.
|
|
428
|
+
* @internal
|
|
429
|
+
*/
|
|
430
|
+
async cachedLookup(cacheKey, lookup) {
|
|
431
|
+
if (Is.empty(this._tenantCache)) {
|
|
432
|
+
return lookup();
|
|
433
|
+
}
|
|
434
|
+
// The expiry is absolute rather than the idle time the cache applies alongside it. Entries
|
|
435
|
+
// are only invalidated in the process that made the change, so without it a tenant under
|
|
436
|
+
// steady traffic on one node would never pick up an edit made on another.
|
|
437
|
+
const cached = await this._tenantCache.getOrSet(cacheKey, lookup, Date.now() + this._tenantCacheTtlMs);
|
|
438
|
+
// Callers get their own copy; the cached instance is shared by every caller and some of
|
|
439
|
+
// them fill in defaults on the tenant they are handed.
|
|
440
|
+
return {
|
|
441
|
+
...cached,
|
|
442
|
+
organizationIdLegacy: Is.array(cached.organizationIdLegacy)
|
|
443
|
+
? [...cached.organizationIdLegacy]
|
|
444
|
+
: undefined
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Remove every cache entry that can resolve to the given tenant.
|
|
449
|
+
* @param tenant The tenant whose cache entries should be dropped.
|
|
450
|
+
* @internal
|
|
451
|
+
*/
|
|
452
|
+
invalidateTenant(tenant) {
|
|
453
|
+
if (Is.empty(this._tenantCache)) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this._tenantCache.delete(`${TenantAdminService._CACHE_PREFIX_ID}${tenant.id}`);
|
|
457
|
+
if (Is.stringValue(tenant.apiKey)) {
|
|
458
|
+
this._tenantCache.delete(this.apiKeyCacheKey(tenant.apiKey));
|
|
459
|
+
}
|
|
460
|
+
if (Is.stringValue(tenant.publicOrigin)) {
|
|
461
|
+
this._tenantCache.delete(`${TenantAdminService._CACHE_PREFIX_PUBLIC_ORIGIN}${tenant.publicOrigin}`);
|
|
462
|
+
}
|
|
463
|
+
const organizationIds = [tenant.organizationId, ...(tenant.organizationIdLegacy ?? [])];
|
|
464
|
+
for (const organizationId of organizationIds) {
|
|
465
|
+
if (Is.stringValue(organizationId)) {
|
|
466
|
+
this._tenantCache.delete(this.organizationIdCacheKey(organizationId, false));
|
|
467
|
+
this._tenantCache.delete(this.organizationIdCacheKey(organizationId, true));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Convert a tenant entity to a tenant model.
|
|
473
|
+
* @param tenant The tenant entity.
|
|
474
|
+
* @returns The tenant model.
|
|
475
|
+
* @internal
|
|
476
|
+
*/
|
|
477
|
+
entityToModel(tenant) {
|
|
478
|
+
return {
|
|
479
|
+
id: tenant.id,
|
|
480
|
+
apiKey: tenant.apiKey,
|
|
481
|
+
label: tenant.label,
|
|
482
|
+
dateCreated: tenant.dateCreated,
|
|
483
|
+
dateModified: tenant.dateModified,
|
|
484
|
+
publicOrigin: tenant.publicOrigin,
|
|
485
|
+
organizationId: tenant.organizationId,
|
|
486
|
+
organizationIdLegacy: tenant.organizationIdLegacy
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Convert a tenant model to a tenant entity.
|
|
491
|
+
* @param tenant The tenant model.
|
|
492
|
+
* @returns The tenant entity.
|
|
493
|
+
* @internal
|
|
494
|
+
*/
|
|
495
|
+
modelToEntity(tenant) {
|
|
496
|
+
const tenantEntity = new Tenant();
|
|
497
|
+
tenantEntity.id = tenant.id;
|
|
498
|
+
tenantEntity.apiKey = tenant.apiKey;
|
|
499
|
+
tenantEntity.label = tenant.label;
|
|
500
|
+
tenantEntity.dateCreated = tenant.dateCreated;
|
|
501
|
+
tenantEntity.dateModified = tenant.dateModified;
|
|
502
|
+
tenantEntity.publicOrigin = tenant.publicOrigin;
|
|
503
|
+
tenantEntity.organizationId = tenant.organizationId;
|
|
504
|
+
tenantEntity.organizationIdLegacy = Is.arrayValue(tenant.organizationIdLegacy)
|
|
505
|
+
? tenant.organizationIdLegacy
|
|
506
|
+
: undefined;
|
|
507
|
+
return tenantEntity;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
//# sourceMappingURL=tenantAdminService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenantAdminService.js","sources":["../../src/tenantAdminService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// Copyright 2026 3Sixty Trade Ltd.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\ttype HealthApplicationCallback,\n\tHealthCategory,\n\tHealthStatus,\n\ttype IHealth,\n\ttype IHealthProviderComponent,\n\ttype IPlatformComponent,\n\ttype ITenant,\n\ttype ITenantAdminComponent,\n\tTenantEventType\n} from \"@3sixty/api-models\";\nimport { ContextIdKeys, ContextIdStore, type IContextIds } from \"@3sixty/context\";\nimport {\n\tAlreadyExistsError,\n\tBaseError,\n\tComponentFactory,\n\tConverter,\n\tGeneralError,\n\tGuards,\n\ttype IError,\n\tIs,\n\tLfuCache,\n\tNotFoundError,\n\tUrl\n} from \"@3sixty/core\";\nimport { Blake2b } from \"@3sixty/crypto\";\nimport { ComparisonOperator, type EntityCondition } from \"@3sixty/entity\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@3sixty/entity-storage-models\";\n\nimport { Tenant } from \"./entities/tenant.js\";\nimport type { ITenantAdminServiceConstructorOptions } from \"./models/ITenantAdminServiceConstructorOptions.js\";\nimport { TenantIdHelper } from \"./utils/tenantIdHelper.js\";\n\n/**\n * Service for performing tenant administration operations.\n */\nexport class TenantAdminService implements ITenantAdminComponent, IHealthProviderComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = \"TenantAdminService\";\n\n\t/**\n\t * The default time to keep a tenant lookup in the cache.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_CACHE_TTL_MS: number = 30000;\n\n\t/**\n\t * The cache key prefix for tenants looked up by id.\n\t * @internal\n\t */\n\tprivate static readonly _CACHE_PREFIX_ID: string = \"id:\";\n\n\t/**\n\t * The cache key prefix for tenants looked up by api key.\n\t * @internal\n\t */\n\tprivate static readonly _CACHE_PREFIX_API_KEY: string = \"apiKey:\";\n\n\t/**\n\t * The cache key prefix for tenants looked up by public origin.\n\t * @internal\n\t */\n\tprivate static readonly _CACHE_PREFIX_PUBLIC_ORIGIN: string = \"publicOrigin:\";\n\n\t/**\n\t * The cache key prefix for tenants looked up by organization id, including legacy ids.\n\t * @internal\n\t */\n\tprivate static readonly _CACHE_PREFIX_ORGANIZATION_ID: string = \"organizationId:\";\n\n\t/**\n\t * Entity storage connector used by the service.\n\t * @internal\n\t */\n\tprivate readonly _entityStorageConnector: IEntityStorageConnector<Tenant>;\n\n\t/**\n\t * Platform component used to fire tenant events.\n\t * @internal\n\t */\n\tprivate readonly _platformComponent: IPlatformComponent;\n\n\t/**\n\t * The cache of tenant lookups, undefined when caching is disabled.\n\t * @internal\n\t */\n\tprivate readonly _tenantCache?: LfuCache<ITenant>;\n\n\t/**\n\t * How long a cached tenant may live, counted from when it was read.\n\t * @internal\n\t */\n\tprivate readonly _tenantCacheTtlMs: number;\n\n\t/**\n\t * Create a new instance of TenantAdminService.\n\t * @param options The options for the connector.\n\t */\n\tconstructor(options?: ITenantAdminServiceConstructorOptions) {\n\t\tthis._entityStorageConnector = EntityStorageConnectorFactory.get(\n\t\t\toptions?.tenantEntityStorageType ?? \"tenant\"\n\t\t);\n\t\tthis._platformComponent = ComponentFactory.get<IPlatformComponent>(\n\t\t\toptions?.platformComponentType ?? \"platform\"\n\t\t);\n\n\t\tthis._tenantCacheTtlMs =\n\t\t\toptions?.config?.tenantCacheTtlMs ?? TenantAdminService._DEFAULT_CACHE_TTL_MS;\n\t\tif (this._tenantCacheTtlMs > 0) {\n\t\t\tthis._tenantCache = new LfuCache<ITenant>({\n\t\t\t\tcapacity: options?.config?.tenantCacheCapacity,\n\t\t\t\tttiMs: this._tenantCacheTtlMs,\n\t\t\t\tmutexTimeoutMs: options?.config?.tenantCacheMutexTimeoutMs\n\t\t\t});\n\t\t}\n\t}\n\n\t/**\n\t * Returns the class name of the component.\n\t * @returns The class name of the component.\n\t */\n\tpublic className(): string {\n\t\treturn TenantAdminService.CLASS_NAME;\n\t}\n\n\t/**\n\t * Stop the service and release the tenant cache.\n\t * @returns A promise that resolves when the service has stopped.\n\t */\n\tpublic async stop(): Promise<void> {\n\t\tthis._tenantCache?.destroy();\n\t}\n\n\t/**\n\t * Get a tenant by its id.\n\t * @param tenantId The id of the tenant.\n\t * @returns The tenant.\n\t * @throws Error if the tenant is not found.\n\t */\n\tpublic async get(tenantId: string): Promise<ITenant> {\n\t\tGuards.stringHexLength(TenantAdminService.CLASS_NAME, \"tenantId\", tenantId, 32);\n\n\t\treturn this.cachedLookup(`${TenantAdminService._CACHE_PREFIX_ID}${tenantId}`, async () => {\n\t\t\tconst tenant = await this._entityStorageConnector.get(tenantId);\n\t\t\tif (!Is.object(tenant)) {\n\t\t\t\tthrow new NotFoundError(TenantAdminService.CLASS_NAME, \"tenantNotFound\", tenantId);\n\t\t\t}\n\n\t\t\treturn this.entityToModel(tenant);\n\t\t});\n\t}\n\n\t/**\n\t * Get a tenant by its api key.\n\t * @param apiKey The api key of the tenant.\n\t * @returns The tenant.\n\t * @throws Error if the tenant is not found.\n\t */\n\tpublic async getByApiKey(apiKey: string): Promise<ITenant> {\n\t\tGuards.stringHexLength(TenantAdminService.CLASS_NAME, \"apiKey\", apiKey, 32);\n\n\t\treturn this.cachedLookup(this.apiKeyCacheKey(apiKey), async () => {\n\t\t\tconst tenant = await this._entityStorageConnector.get(apiKey, \"apiKey\");\n\t\t\tif (!Is.object(tenant)) {\n\t\t\t\tthrow new NotFoundError(TenantAdminService.CLASS_NAME, \"tenantNotFound\", apiKey);\n\t\t\t}\n\n\t\t\treturn this.entityToModel(tenant);\n\t\t});\n\t}\n\n\t/**\n\t * Get a tenant by its public origin.\n\t * @param publicOrigin The origin of the tenant.\n\t * @returns The tenant.\n\t * @throws Error if the tenant is not found.\n\t */\n\tpublic async getByPublicOrigin(publicOrigin: string): Promise<ITenant> {\n\t\tGuards.stringValue(TenantAdminService.CLASS_NAME, \"publicOrigin\", publicOrigin);\n\n\t\treturn this.cachedLookup(\n\t\t\t`${TenantAdminService._CACHE_PREFIX_PUBLIC_ORIGIN}${publicOrigin}`,\n\t\t\tasync () => {\n\t\t\t\tconst tenant = await this._entityStorageConnector.get(publicOrigin, \"publicOrigin\");\n\n\t\t\t\tif (!Is.object(tenant)) {\n\t\t\t\t\tthrow new NotFoundError(TenantAdminService.CLASS_NAME, \"tenantNotFound\", publicOrigin);\n\t\t\t\t}\n\n\t\t\t\treturn this.entityToModel(tenant);\n\t\t\t}\n\t\t);\n\t}\n\n\t/**\n\t * Get a tenant by its organization id, optionally searching legacy ids.\n\t * @param organizationId The organization id of the tenant.\n\t * @param includeLegacy Whether to also search the legacy organization id array.\n\t * @returns The tenant.\n\t * @throws Error if the tenant is not found.\n\t */\n\tpublic async getTenantByOrganizationId(\n\t\torganizationId: string,\n\t\tincludeLegacy?: boolean\n\t): Promise<ITenant> {\n\t\tGuards.stringValue(TenantAdminService.CLASS_NAME, \"organizationId\", organizationId);\n\n\t\treturn this.cachedLookup(\n\t\t\tthis.organizationIdCacheKey(organizationId, includeLegacy ?? false),\n\t\t\tasync () => {\n\t\t\t\tlet tenant = await this._entityStorageConnector.get(organizationId, \"organizationId\");\n\n\t\t\t\tif (!Is.object(tenant) && (includeLegacy ?? false)) {\n\t\t\t\t\tconst result = await this._entityStorageConnector.query(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tproperty: \"organizationIdLegacy\",\n\t\t\t\t\t\t\tcomparison: ComparisonOperator.Includes,\n\t\t\t\t\t\t\tvalue: organizationId\n\t\t\t\t\t\t},\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\t1\n\t\t\t\t\t);\n\t\t\t\t\ttenant = (result.entities as Tenant[])[0];\n\t\t\t\t}\n\n\t\t\t\tif (!Is.object(tenant)) {\n\t\t\t\t\tthrow new NotFoundError(TenantAdminService.CLASS_NAME, \"tenantNotFound\", organizationId);\n\t\t\t\t}\n\n\t\t\t\treturn this.entityToModel(tenant);\n\t\t\t}\n\t\t);\n\t}\n\n\t/**\n\t * Create a tenant.\n\t * @param tenant The tenant to store.\n\t * @returns The tenant id.\n\t */\n\tpublic async create(\n\t\ttenant: Omit<ITenant, \"id\" | \"dateCreated\" | \"dateModified\"> & { id?: string }\n\t): Promise<string> {\n\t\tGuards.objectValue<ITenant>(TenantAdminService.CLASS_NAME, \"tenant\", tenant);\n\t\tGuards.stringValue(\n\t\t\tTenantAdminService.CLASS_NAME,\n\t\t\t\"tenant.organizationId\",\n\t\t\ttenant.organizationId\n\t\t);\n\t\tif (Is.stringValue(tenant.id)) {\n\t\t\tGuards.stringHexLength(TenantAdminService.CLASS_NAME, \"tenant.id\", tenant.id, 32);\n\t\t\tconst existing = await this._entityStorageConnector.get(tenant.id);\n\t\t\tif (Is.object(existing)) {\n\t\t\t\tthrow new AlreadyExistsError(\n\t\t\t\t\tTenantAdminService.CLASS_NAME,\n\t\t\t\t\t\"tenantAlreadyExists\",\n\t\t\t\t\ttenant.id\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tif (Is.stringValue(tenant.apiKey)) {\n\t\t\tGuards.stringHexLength(\n\t\t\t\tTenantAdminService.CLASS_NAME,\n\t\t\t\t\"tenant.apiKey\",\n\t\t\t\ttenant.apiKey,\n\t\t\t\t32\n\t\t\t);\n\t\t}\n\n\t\tlet publicOrigin: string | undefined;\n\t\tif (Is.stringValue(tenant.publicOrigin)) {\n\t\t\tUrl.guard(TenantAdminService.CLASS_NAME, \"tenant.publicOrigin\", tenant.publicOrigin);\n\n\t\t\tconst url = new Url(tenant.publicOrigin);\n\t\t\tconst parts = url.parts();\n\t\t\tpublicOrigin = `${parts.schema}://${parts.host}${Is.integer(parts.port) ? `:${parts.port}` : \"\"}`;\n\t\t}\n\n\t\tif (Is.stringValue(tenant.apiKey)) {\n\t\t\tconst existingApiKey = await this._entityStorageConnector.get(tenant.apiKey, \"apiKey\");\n\t\t\tif (Is.object(existingApiKey) && existingApiKey.id !== tenant.id) {\n\t\t\t\tthrow new GeneralError(TenantAdminService.CLASS_NAME, \"apiKeyAlreadyInUse\");\n\t\t\t}\n\t\t}\n\n\t\tif (Is.stringValue(publicOrigin)) {\n\t\t\tconst existingPublicOrigin = await this._entityStorageConnector.get(\n\t\t\t\tpublicOrigin,\n\t\t\t\t\"publicOrigin\"\n\t\t\t);\n\t\t\tif (Is.object(existingPublicOrigin)) {\n\t\t\t\tthrow new AlreadyExistsError(\n\t\t\t\t\tTenantAdminService.CLASS_NAME,\n\t\t\t\t\t\"publicOriginAlreadyExists\",\n\t\t\t\t\tpublicOrigin\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst existingOrgId = await this._entityStorageConnector.get(\n\t\t\ttenant.organizationId,\n\t\t\t\"organizationId\"\n\t\t);\n\t\tif (Is.object(existingOrgId)) {\n\t\t\tthrow new AlreadyExistsError(\n\t\t\t\tTenantAdminService.CLASS_NAME,\n\t\t\t\t\"organizationIdAlreadyExists\",\n\t\t\t\ttenant.organizationId\n\t\t\t);\n\t\t}\n\n\t\tconst tenantEntity: ITenant = {\n\t\t\tid: tenant.id ?? TenantIdHelper.generateTenantId(),\n\t\t\tapiKey: tenant.apiKey ?? TenantIdHelper.generateApiKey(),\n\t\t\tdateCreated: new Date(Date.now()).toISOString(),\n\t\t\tdateModified: new Date(Date.now()).toISOString(),\n\t\t\tlabel: tenant.label,\n\t\t\tpublicOrigin,\n\t\t\torganizationId: tenant.organizationId,\n\t\t\torganizationIdLegacy: tenant.organizationIdLegacy\n\t\t};\n\n\t\tawait this._entityStorageConnector.set(this.modelToEntity(tenantEntity));\n\n\t\t// A new tenant cannot have a stale id, api key or public origin entry, because those are\n\t\t// unique-checked above and failed lookups are never cached. Its organization ids can be\n\t\t// stale though; only the primary organization index is checked, so an id that another\n\t\t// tenant currently answers through its legacy list may already be cached against that\n\t\t// tenant even though the new tenant now owns it outright.\n\t\tthis.invalidateTenant(tenantEntity);\n\n\t\tawait this._platformComponent.fireTenantEvent(tenantEntity.id, TenantEventType.Created);\n\n\t\treturn tenantEntity.id;\n\t}\n\n\t/**\n\t * Update a tenant.\n\t * @param tenant The tenant to update.\n\t * @returns A promise that resolves when the tenant has been updated.\n\t */\n\tpublic async update(\n\t\ttenant: Partial<Omit<ITenant, \"dateCreated\" | \"dateModified\">>\n\t): Promise<void> {\n\t\tGuards.objectValue<ITenant>(TenantAdminService.CLASS_NAME, \"tenant\", tenant);\n\t\tGuards.stringHexLength(TenantAdminService.CLASS_NAME, \"tenant.id\", tenant.id, 32);\n\t\tGuards.stringValue(\n\t\t\tTenantAdminService.CLASS_NAME,\n\t\t\t\"tenant.organizationId\",\n\t\t\ttenant.organizationId\n\t\t);\n\t\tif (Is.stringValue(tenant.apiKey)) {\n\t\t\tGuards.stringHexLength(\n\t\t\t\tTenantAdminService.CLASS_NAME,\n\t\t\t\t\"tenant.apiKey\",\n\t\t\t\ttenant.apiKey,\n\t\t\t\t32\n\t\t\t);\n\t\t}\n\n\t\tlet publicOrigin: string | undefined;\n\t\tif (Is.stringValue(tenant.publicOrigin)) {\n\t\t\tUrl.guard(TenantAdminService.CLASS_NAME, \"tenant.publicOrigin\", tenant.publicOrigin);\n\n\t\t\tconst url = new Url(tenant.publicOrigin);\n\t\t\tconst parts = url.parts();\n\t\t\tpublicOrigin = `${parts.schema}://${parts.host}${Is.integer(parts.port) ? `:${parts.port}` : \"\"}`;\n\t\t}\n\n\t\tconst currentTenant = await this._entityStorageConnector.get(tenant.id);\n\t\tif (!Is.object(currentTenant)) {\n\t\t\tthrow new NotFoundError(TenantAdminService.CLASS_NAME, \"tenantNotFound\", tenant.id);\n\t\t}\n\n\t\tif (Is.stringValue(tenant.apiKey)) {\n\t\t\tconst existingApiKey = await this._entityStorageConnector.get(tenant.apiKey, \"apiKey\");\n\t\t\tif (Is.object(existingApiKey) && existingApiKey.id !== currentTenant.id) {\n\t\t\t\tthrow new GeneralError(TenantAdminService.CLASS_NAME, \"apiKeyAlreadyInUse\");\n\t\t\t}\n\t\t}\n\n\t\tif (Is.stringValue(publicOrigin)) {\n\t\t\tconst existingPublicOrigin = await this._entityStorageConnector.get(\n\t\t\t\tpublicOrigin,\n\t\t\t\t\"publicOrigin\"\n\t\t\t);\n\t\t\tif (Is.object(existingPublicOrigin) && existingPublicOrigin.id !== currentTenant.id) {\n\t\t\t\tthrow new AlreadyExistsError(\n\t\t\t\t\tTenantAdminService.CLASS_NAME,\n\t\t\t\t\t\"publicOriginAlreadyExists\",\n\t\t\t\t\tpublicOrigin\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst existingOrgId = await this._entityStorageConnector.get(\n\t\t\ttenant.organizationId,\n\t\t\t\"organizationId\"\n\t\t);\n\t\tif (Is.object(existingOrgId) && existingOrgId.id !== currentTenant.id) {\n\t\t\tthrow new AlreadyExistsError(\n\t\t\t\tTenantAdminService.CLASS_NAME,\n\t\t\t\t\"organizationIdAlreadyExists\",\n\t\t\t\ttenant.organizationId\n\t\t\t);\n\t\t}\n\n\t\tconst currentTenantEntity = this.entityToModel(currentTenant);\n\n\t\tconst orgIdChanged = tenant.organizationId !== currentTenantEntity.organizationId;\n\n\t\tlet newOrganizationIdLegacy: string[] | undefined;\n\t\tif (orgIdChanged) {\n\t\t\tconst legacySet = new Set(currentTenantEntity.organizationIdLegacy ?? []);\n\t\t\tif (Is.stringValue(currentTenantEntity.organizationId)) {\n\t\t\t\tlegacySet.add(currentTenantEntity.organizationId);\n\t\t\t}\n\t\t\tlegacySet.delete(tenant.organizationId);\n\t\t\tnewOrganizationIdLegacy = legacySet.size > 0 ? [...legacySet] : undefined;\n\t\t} else {\n\t\t\tnewOrganizationIdLegacy = Is.array(tenant.organizationIdLegacy)\n\t\t\t\t? tenant.organizationIdLegacy\n\t\t\t\t: currentTenantEntity.organizationIdLegacy;\n\t\t}\n\n\t\tconst tenantEntity: ITenant = {\n\t\t\tid: tenant.id,\n\t\t\tapiKey: tenant.apiKey ?? currentTenantEntity.apiKey,\n\t\t\tdateCreated: currentTenantEntity.dateCreated,\n\t\t\tdateModified: new Date(Date.now()).toISOString(),\n\t\t\tlabel: tenant.label ?? currentTenantEntity.label,\n\t\t\tpublicOrigin: publicOrigin ?? currentTenantEntity.publicOrigin,\n\t\t\torganizationId: tenant.organizationId,\n\t\t\torganizationIdLegacy: newOrganizationIdLegacy\n\t\t};\n\n\t\tawait this._entityStorageConnector.set(this.modelToEntity(tenantEntity));\n\n\t\t// The api key, organization id and public origin can all have moved, so the entries\n\t\t// cached under the previous values have to go as well as the new ones.\n\t\tthis.invalidateTenant(currentTenantEntity);\n\t\tthis.invalidateTenant(tenantEntity);\n\n\t\tawait this._platformComponent.fireTenantEvent(tenantEntity.id, TenantEventType.Updated);\n\t}\n\n\t/**\n\t * Remove a tenant by its id.\n\t * @param tenantId The id of the tenant.\n\t * @returns Nothing.\n\t */\n\tpublic async remove(tenantId: string): Promise<void> {\n\t\tGuards.stringHexLength(TenantAdminService.CLASS_NAME, \"tenantId\", tenantId, 32);\n\n\t\t// The keys to drop have to be read before the tenant goes, but the drop itself has to happen\n\t\t// after it, otherwise a lookup racing the delete would re-cache the tenant behind us.\n\t\tconst currentTenant = Is.empty(this._tenantCache)\n\t\t\t? undefined\n\t\t\t: await this._entityStorageConnector.get(tenantId);\n\n\t\tawait this._entityStorageConnector.remove(tenantId);\n\n\t\tif (Is.object(currentTenant)) {\n\t\t\tthis.invalidateTenant(this.entityToModel(currentTenant));\n\t\t} else {\n\t\t\t// The tenant could not be read back, so its other keys are unknown, but an entry cached\n\t\t\t// under the id before it went missing still has to go.\n\t\t\tthis._tenantCache?.delete(`${TenantAdminService._CACHE_PREFIX_ID}${tenantId}`);\n\t\t}\n\n\t\tawait this._platformComponent.fireTenantEvent(tenantId, TenantEventType.Deleted);\n\t}\n\n\t/**\n\t * Query tenants with pagination.\n\t * @param conditions The conditions to filter the tenants.\n\t * @param properties The properties to include in the returned tenants.\n\t * @param cursor The cursor to start from.\n\t * @param limit The maximum number of tenants to return.\n\t * @returns The tenants and the next cursor if more tenants are available.\n\t */\n\tpublic async query(\n\t\tconditions?: EntityCondition<ITenant>,\n\t\tproperties?: (keyof ITenant)[],\n\t\tcursor?: string,\n\t\tlimit?: number\n\t): Promise<{ tenants: ITenant[]; cursor?: string }> {\n\t\tconst result = await this._entityStorageConnector.query(\n\t\t\tconditions,\n\t\t\tundefined,\n\t\t\tproperties,\n\t\t\tcursor,\n\t\t\tlimit\n\t\t);\n\n\t\treturn {\n\t\t\ttenants: (result.entities as Tenant[]).map(e => this.entityToModel(e)),\n\t\t\tcursor: result.cursor\n\t\t};\n\t}\n\n\t/**\n\t * Provision a temporary tenant for the application health cycle.\n\t * @param contextIds Accumulated context IDs; receives the provisional tenant ID.\n\t * @returns A promise that resolves when provisioning is complete.\n\t */\n\tpublic async healthApplicationInit(contextIds: IContextIds): Promise<void> {\n\t\tconst organizationId = contextIds[ContextIdKeys.Organization];\n\t\tif (!Is.stringValue(organizationId)) {\n\t\t\treturn;\n\t\t}\n\t\tconst tenantId = await this.create({\n\t\t\tapiKey: TenantIdHelper.generateApiKey(),\n\t\t\torganizationId,\n\t\t\tlabel: \"health-check\"\n\t\t});\n\t\tcontextIds[ContextIdKeys.Tenant] = tenantId;\n\t}\n\n\t/**\n\t * Verify the provisioned tenant can be retrieved.\n\t * The tenant ID is read from the active context set by the init pass.\n\t * @param callback The callback to invoke when a deferred health result is ready.\n\t * @returns The health entries for this component.\n\t */\n\tpublic async healthApplication(\n\t\tcallback: HealthApplicationCallback\n\t): Promise<IHealth[] | undefined> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tconst tenantId = contextIds?.[ContextIdKeys.Tenant];\n\n\t\tif (!Is.stringValue(tenantId)) {\n\t\t\treturn [];\n\t\t}\n\n\t\tlet status: HealthStatus = HealthStatus.Ok;\n\t\tlet healthError: IError | undefined;\n\n\t\ttry {\n\t\t\tawait this.get(tenantId);\n\t\t} catch (err) {\n\t\t\tstatus = HealthStatus.Error;\n\t\t\thealthError = BaseError.fromError(err);\n\t\t}\n\n\t\treturn [\n\t\t\t{\n\t\t\t\tsource: TenantAdminService.CLASS_NAME,\n\t\t\t\tcategory: HealthCategory.Application,\n\t\t\t\tstatus,\n\t\t\t\terror: healthError\n\t\t\t}\n\t\t];\n\t}\n\n\t/**\n\t * Remove the temporary tenant provisioned during init.\n\t * The tenant ID is read from the active context set by the init pass.\n\t * Does nothing when no tenant ID is present.\n\t * @returns A promise that resolves when teardown is complete.\n\t */\n\tpublic async healthApplicationTeardown(): Promise<void> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tconst tenantId = contextIds?.[ContextIdKeys.Tenant];\n\n\t\tif (Is.stringValue(tenantId)) {\n\t\t\tawait this.remove(tenantId);\n\t\t}\n\t}\n\n\t/**\n\t * Build the cache key used for an api key lookup.\n\t * The api key is hashed so neither the cache keys nor the mutex keys derived from them hold\n\t * the credential itself.\n\t * @param apiKey The api key being looked up.\n\t * @returns The cache key.\n\t * @internal\n\t */\n\tprivate apiKeyCacheKey(apiKey: string): string {\n\t\tconst hashed = Converter.bytesToHex(Blake2b.sum256(Converter.utf8ToBytes(apiKey)));\n\t\treturn `${TenantAdminService._CACHE_PREFIX_API_KEY}${hashed}`;\n\t}\n\n\t/**\n\t * Build the cache key used for an organization id lookup.\n\t * @param organizationId The organization id being looked up.\n\t * @param includeLegacy Whether the lookup also searches the legacy organization ids.\n\t * @returns The cache key.\n\t * @internal\n\t */\n\tprivate organizationIdCacheKey(organizationId: string, includeLegacy: boolean): string {\n\t\treturn `${TenantAdminService._CACHE_PREFIX_ORGANIZATION_ID}${includeLegacy ? \"legacy:\" : \"\"}${organizationId}`;\n\t}\n\n\t/**\n\t * Perform a lookup through the cache, falling back to a direct lookup when caching is disabled.\n\t * Lookups that throw are not cached, so a missing tenant is re-checked against storage each time.\n\t * @param cacheKey The key the tenant is cached under.\n\t * @param lookup The lookup to perform when the tenant is not already cached.\n\t * @returns The tenant.\n\t * @internal\n\t */\n\tprivate async cachedLookup(cacheKey: string, lookup: () => Promise<ITenant>): Promise<ITenant> {\n\t\tif (Is.empty(this._tenantCache)) {\n\t\t\treturn lookup();\n\t\t}\n\n\t\t// The expiry is absolute rather than the idle time the cache applies alongside it. Entries\n\t\t// are only invalidated in the process that made the change, so without it a tenant under\n\t\t// steady traffic on one node would never pick up an edit made on another.\n\t\tconst cached = await this._tenantCache.getOrSet(\n\t\t\tcacheKey,\n\t\t\tlookup,\n\t\t\tDate.now() + this._tenantCacheTtlMs\n\t\t);\n\n\t\t// Callers get their own copy; the cached instance is shared by every caller and some of\n\t\t// them fill in defaults on the tenant they are handed.\n\t\treturn {\n\t\t\t...cached,\n\t\t\torganizationIdLegacy: Is.array(cached.organizationIdLegacy)\n\t\t\t\t? [...cached.organizationIdLegacy]\n\t\t\t\t: undefined\n\t\t};\n\t}\n\n\t/**\n\t * Remove every cache entry that can resolve to the given tenant.\n\t * @param tenant The tenant whose cache entries should be dropped.\n\t * @internal\n\t */\n\tprivate invalidateTenant(tenant: ITenant): void {\n\t\tif (Is.empty(this._tenantCache)) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis._tenantCache.delete(`${TenantAdminService._CACHE_PREFIX_ID}${tenant.id}`);\n\n\t\tif (Is.stringValue(tenant.apiKey)) {\n\t\t\tthis._tenantCache.delete(this.apiKeyCacheKey(tenant.apiKey));\n\t\t}\n\n\t\tif (Is.stringValue(tenant.publicOrigin)) {\n\t\t\tthis._tenantCache.delete(\n\t\t\t\t`${TenantAdminService._CACHE_PREFIX_PUBLIC_ORIGIN}${tenant.publicOrigin}`\n\t\t\t);\n\t\t}\n\n\t\tconst organizationIds = [tenant.organizationId, ...(tenant.organizationIdLegacy ?? [])];\n\t\tfor (const organizationId of organizationIds) {\n\t\t\tif (Is.stringValue(organizationId)) {\n\t\t\t\tthis._tenantCache.delete(this.organizationIdCacheKey(organizationId, false));\n\t\t\t\tthis._tenantCache.delete(this.organizationIdCacheKey(organizationId, true));\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Convert a tenant entity to a tenant model.\n\t * @param tenant The tenant entity.\n\t * @returns The tenant model.\n\t * @internal\n\t */\n\tprivate entityToModel(tenant: Tenant): ITenant {\n\t\treturn {\n\t\t\tid: tenant.id,\n\t\t\tapiKey: tenant.apiKey,\n\t\t\tlabel: tenant.label,\n\t\t\tdateCreated: tenant.dateCreated,\n\t\t\tdateModified: tenant.dateModified,\n\t\t\tpublicOrigin: tenant.publicOrigin,\n\t\t\torganizationId: tenant.organizationId,\n\t\t\torganizationIdLegacy: tenant.organizationIdLegacy\n\t\t};\n\t}\n\n\t/**\n\t * Convert a tenant model to a tenant entity.\n\t * @param tenant The tenant model.\n\t * @returns The tenant entity.\n\t * @internal\n\t */\n\tprivate modelToEntity(tenant: ITenant): Tenant {\n\t\tconst tenantEntity = new Tenant();\n\t\ttenantEntity.id = tenant.id;\n\t\ttenantEntity.apiKey = tenant.apiKey;\n\t\ttenantEntity.label = tenant.label;\n\t\ttenantEntity.dateCreated = tenant.dateCreated;\n\t\ttenantEntity.dateModified = tenant.dateModified;\n\t\ttenantEntity.publicOrigin = tenant.publicOrigin;\n\t\ttenantEntity.organizationId = tenant.organizationId;\n\t\ttenantEntity.organizationIdLegacy = Is.arrayValue(tenant.organizationIdLegacy)\n\t\t\t? tenant.organizationIdLegacy\n\t\t\t: undefined;\n\t\treturn tenantEntity;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Converter, Guards, Is } from "@3sixty/core";
|
|
2
|
+
/**
|
|
3
|
+
* Context ID handler that treats a tenant ID as a compact base64url-encoded hex string.
|
|
4
|
+
*/
|
|
5
|
+
export class TenantIdContextIdHandler {
|
|
6
|
+
/**
|
|
7
|
+
* Runtime name for the class.
|
|
8
|
+
*/
|
|
9
|
+
static CLASS_NAME = "TenantIdContextIdHandler";
|
|
10
|
+
/**
|
|
11
|
+
* The class name of the component.
|
|
12
|
+
* @returns The class name.
|
|
13
|
+
*/
|
|
14
|
+
className() {
|
|
15
|
+
return TenantIdContextIdHandler.CLASS_NAME;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The short form of the tenant id is the base64 version to compact.
|
|
19
|
+
* @param value The full context id value.
|
|
20
|
+
* @returns Short form string.
|
|
21
|
+
*/
|
|
22
|
+
short(value) {
|
|
23
|
+
return Converter.bytesToBase64Url(Converter.hexToBytes(value));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The long form version of the context ID, expanded from a short version.
|
|
27
|
+
* @param value The short form context ID value.
|
|
28
|
+
* @returns The long form version of the context ID.
|
|
29
|
+
*/
|
|
30
|
+
long(value) {
|
|
31
|
+
if (Is.stringBase64Url(value)) {
|
|
32
|
+
const bytes = Converter.base64UrlToBytes(value);
|
|
33
|
+
const hex = Converter.bytesToHex(bytes);
|
|
34
|
+
return hex;
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Guard the value ensuring length.
|
|
40
|
+
* @param value The value to guard.
|
|
41
|
+
* @throws GeneralError if the value is too short.
|
|
42
|
+
*/
|
|
43
|
+
guard(value) {
|
|
44
|
+
Guards.stringHexLength(TenantIdContextIdHandler.CLASS_NAME, "value", value, 32);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=tenantIdContextIdHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenantIdContextIdHandler.js","sources":["../../src/tenantIdContextIdHandler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAGrD;;GAEG;AACH,MAAM,OAAO,wBAAwB;IACpC;;OAEG;IACI,MAAM,CAAU,UAAU,GAAW,0BAA0B,CAAC;IAEvE;;;OAGG;IACI,SAAS;QACf,OAAO,wBAAwB,CAAC,UAAU,CAAC;IAC5C,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,KAAa;QACzB,OAAO,SAAS,CAAC,gBAAgB,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;IAChE,CAAC;IAED;;;;OAIG;IACI,IAAI,CAAC,KAAa;QACxB,IAAI,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,KAAK,GAAG,SAAS,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;YAChD,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;YACxC,OAAO,GAAG,CAAC;QACZ,CAAC;QACD,OAAO,KAAK,CAAC;IACd,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,KAAa;QACzB,MAAM,CAAC,eAAe,CAAC,wBAAwB,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IACjF,CAAC;CACD","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IContextIdHandler } from \"@3sixty/context\";\nimport { Converter, Guards, Is } from \"@3sixty/core\";\n\n\n/**\n * Context ID handler that treats a tenant ID as a compact base64url-encoded hex string.\n */\nexport class TenantIdContextIdHandler implements IContextIdHandler {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = \"TenantIdContextIdHandler\";\n\n\t/**\n\t * The class name of the component.\n\t * @returns The class name.\n\t */\n\tpublic className(): string {\n\t\treturn TenantIdContextIdHandler.CLASS_NAME;\n\t}\n\n\t/**\n\t * The short form of the tenant id is the base64 version to compact.\n\t * @param value The full context id value.\n\t * @returns Short form string.\n\t */\n\tpublic short(value: string): string {\n\t\treturn Converter.bytesToBase64Url(Converter.hexToBytes(value));\n\t}\n\n\t/**\n\t * The long form version of the context ID, expanded from a short version.\n\t * @param value The short form context ID value.\n\t * @returns The long form version of the context ID.\n\t */\n\tpublic long(value: string): string {\n\t\tif (Is.stringBase64Url(value)) {\n\t\t\tconst bytes = Converter.base64UrlToBytes(value);\n\t\t\tconst hex = Converter.bytesToHex(bytes);\n\t\t\treturn hex;\n\t\t}\n\t\treturn value;\n\t}\n\n\t/**\n\t * Guard the value ensuring length.\n\t * @param value The value to guard.\n\t * @throws GeneralError if the value is too short.\n\t */\n\tpublic guard(value: string): void {\n\t\tGuards.stringHexLength(TenantIdContextIdHandler.CLASS_NAME, \"value\", value, 32);\n\t}\n}\n"]}
|