@twin.org/api-tenant-processor 0.0.3-next.28 → 0.0.3-next.29
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/dist/es/index.js +0 -1
- package/dist/es/index.js.map +1 -1
- package/dist/es/models/ITenantProcessorConfig.js.map +1 -1
- package/dist/es/models/ITenantProcessorConstructorOptions.js.map +1 -1
- package/dist/es/tenantProcessor.js +51 -122
- package/dist/es/tenantProcessor.js.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/models/ITenantProcessorConfig.d.ts +0 -10
- package/dist/types/models/ITenantProcessorConstructorOptions.d.ts +3 -4
- package/dist/types/tenantProcessor.d.ts +1 -9
- package/docs/changelog.md +14 -0
- package/docs/reference/classes/TenantProcessor.md +1 -29
- package/docs/reference/index.md +0 -1
- package/docs/reference/interfaces/ITenantProcessorConfig.md +0 -28
- package/docs/reference/interfaces/ITenantProcessorConstructorOptions.md +4 -5
- package/locales/en.json +2 -7
- package/package.json +2 -2
- package/dist/es/utils/tenantUrlHelper.js +0 -65
- package/dist/es/utils/tenantUrlHelper.js.map +0 -1
- package/dist/types/utils/tenantUrlHelper.d.ts +0 -34
- package/docs/reference/classes/TenantUrlHelper.md +0 -111
package/dist/es/index.js
CHANGED
package/dist/es/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sCAAsC,CAAC;AACrD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,uCAAuC,CAAC;AACtD,cAAc,iDAAiD,CAAC;AAChE,cAAc,oCAAoC,CAAC;AACnD,cAAc,oCAAoC,CAAC;AACnD,cAAc,qCAAqC,CAAC;AACpD,cAAc,sCAAsC,CAAC;AACrD,cAAc,sCAAsC,CAAC;AACrD,cAAc,uCAAuC,CAAC;AACtD,cAAc,mDAAmD,CAAC;AAClE,cAAc,oCAAoC,CAAC;AACnD,cAAc,gDAAgD,CAAC;AAC/D,cAAc,aAAa,CAAC;AAC5B,cAAc,yBAAyB,CAAC;AACxC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,2BAA2B,CAAC
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sCAAsC,CAAC;AACrD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,uCAAuC,CAAC;AACtD,cAAc,iDAAiD,CAAC;AAChE,cAAc,oCAAoC,CAAC;AACnD,cAAc,oCAAoC,CAAC;AACnD,cAAc,qCAAqC,CAAC;AACpD,cAAc,sCAAsC,CAAC;AACrD,cAAc,sCAAsC,CAAC;AACrD,cAAc,uCAAuC,CAAC;AACtD,cAAc,mDAAmD,CAAC;AAClE,cAAc,oCAAoC,CAAC;AACnD,cAAc,gDAAgD,CAAC;AAC/D,cAAc,aAAa,CAAC;AAC5B,cAAc,yBAAyB,CAAC;AACxC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,2BAA2B,CAAC","sourcesContent":["// Copyright 2025 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./entities/tenant.js\";\nexport * from \"./models/api/ITenantCreateRequest.js\";\nexport * from \"./models/api/ITenantGetByApiKeyRequest.js\";\nexport * from \"./models/api/ITenantGetByIdRequest.js\";\nexport * from \"./models/api/ITenantGetByPublicOriginRequest.js\";\nexport * from \"./models/api/ITenantGetResponse.js\";\nexport * from \"./models/api/ITenantListRequest.js\";\nexport * from \"./models/api/ITenantListResponse.js\";\nexport * from \"./models/api/ITenantRemoveRequest.js\";\nexport * from \"./models/api/ITenantUpdateRequest.js\";\nexport * from \"./models/ITenantAdminServiceConfig.js\";\nexport * from \"./models/ITenantAdminServiceConstructorOptions.js\";\nexport * from \"./models/ITenantProcessorConfig.js\";\nexport * from \"./models/ITenantProcessorConstructorOptions.js\";\nexport * from \"./schema.js\";\nexport * from \"./tenantAdminService.js\";\nexport * from \"./tenantIdContextIdHandler.js\";\nexport * from \"./tenantProcessor.js\";\nexport * from \"./tenantRoutes.js\";\nexport * from \"./utils/tenantIdHelper.js\";\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ITenantProcessorConfig.js","sourceRoot":"","sources":["../../../src/models/ITenantProcessorConfig.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC","sourcesContent":["// Copyright 2025 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\n\n/**\n * Configuration for the tenant processor\n */\nexport interface ITenantProcessorConfig {\n\t/**\n\t * The key to look for in the header or query params for the api key.\n\t * @default x-api-key\n\t */\n\tapiKeyName?: string;\n
|
|
1
|
+
{"version":3,"file":"ITenantProcessorConfig.js","sourceRoot":"","sources":["../../../src/models/ITenantProcessorConfig.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC","sourcesContent":["// Copyright 2025 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\n\n/**\n * Configuration for the tenant processor\n */\nexport interface ITenantProcessorConfig {\n\t/**\n\t * The key to look for in the header or query params for the api key.\n\t * @default x-api-key\n\t */\n\tapiKeyName?: string;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ITenantProcessorConstructorOptions.js","sourceRoot":"","sources":["../../../src/models/ITenantProcessorConstructorOptions.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2025 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { ITenantProcessorConfig } from \"./ITenantProcessorConfig.js\";\n\n/**\n * Options for the Tenant Processor constructor.\n */\nexport interface ITenantProcessorConstructorOptions {\n\t/**\n\t * The entity storage for the tenants.\n\t * @default tenant\n\t */\n\ttenantEntityStorageType?: string;\n\n\t/**\n\t * The
|
|
1
|
+
{"version":3,"file":"ITenantProcessorConstructorOptions.js","sourceRoot":"","sources":["../../../src/models/ITenantProcessorConstructorOptions.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2025 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { ITenantProcessorConfig } from \"./ITenantProcessorConfig.js\";\n\n/**\n * Options for the Tenant Processor constructor.\n */\nexport interface ITenantProcessorConstructorOptions {\n\t/**\n\t * The entity storage for the tenants.\n\t * @default tenant\n\t */\n\ttenantEntityStorageType?: string;\n\n\t/**\n\t * The hosting component for the tenants.\n\t * @default hosting\n\t */\n\thostingComponentType?: string;\n\n\t/**\n\t * Configuration for the processor.\n\t */\n\tconfig?: ITenantProcessorConfig;\n}\n"]}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// Copyright 2025 IOTA Stiftung.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0.
|
|
3
3
|
import { HttpErrorHelper } from "@twin.org/api-models";
|
|
4
|
-
import {
|
|
5
|
-
import { BaseError, Is, UnauthorizedError } from "@twin.org/core";
|
|
4
|
+
import { ContextIdKeys } from "@twin.org/context";
|
|
5
|
+
import { BaseError, ComponentFactory, Is, UnauthorizedError } from "@twin.org/core";
|
|
6
6
|
import { EntityStorageConnectorFactory } from "@twin.org/entity-storage-models";
|
|
7
|
-
import { VaultConnectorFactory } from "@twin.org/vault-models";
|
|
8
7
|
import { HttpStatusCode } from "@twin.org/web";
|
|
9
|
-
import { TenantUrlHelper } from "./utils/tenantUrlHelper.js";
|
|
10
8
|
/**
|
|
11
9
|
* Handles incoming api keys and maps them to tenant ids.
|
|
12
10
|
*/
|
|
@@ -26,47 +24,23 @@ export class TenantProcessor {
|
|
|
26
24
|
*/
|
|
27
25
|
_entityStorageConnector;
|
|
28
26
|
/**
|
|
29
|
-
* The
|
|
30
|
-
* @internal
|
|
31
|
-
*/
|
|
32
|
-
_apiKeyName;
|
|
33
|
-
/**
|
|
34
|
-
* The query param name carrying the encrypted tenant token.
|
|
35
|
-
* @internal
|
|
36
|
-
*/
|
|
37
|
-
_tenantTokenName;
|
|
38
|
-
/**
|
|
39
|
-
* The vault connector used to decrypt tenant tokens.
|
|
40
|
-
* Only set when `signingKeyName` is configured.
|
|
41
|
-
* @internal
|
|
42
|
-
*/
|
|
43
|
-
_vaultConnector;
|
|
44
|
-
/**
|
|
45
|
-
* The name of the symmetric key in the vault used to decrypt tenant tokens.
|
|
27
|
+
* The hosting component, used to resolve public origins for tenants and encrypt/decrypt tenant tokens.
|
|
46
28
|
* @internal
|
|
47
29
|
*/
|
|
48
|
-
|
|
30
|
+
_hostingComponent;
|
|
49
31
|
/**
|
|
50
|
-
* The
|
|
32
|
+
* The key in the header to look for the api key.
|
|
51
33
|
* @internal
|
|
52
34
|
*/
|
|
53
|
-
|
|
35
|
+
_apiKeyName;
|
|
54
36
|
/**
|
|
55
|
-
* Create a new instance of
|
|
37
|
+
* Create a new instance of TenantProcessor.
|
|
56
38
|
* @param options Options for the processor.
|
|
57
39
|
*/
|
|
58
40
|
constructor(options) {
|
|
59
41
|
this._entityStorageConnector = EntityStorageConnectorFactory.get(options?.tenantEntityStorageType ?? "tenant");
|
|
42
|
+
this._hostingComponent = ComponentFactory.get(options?.hostingComponentType ?? "hosting");
|
|
60
43
|
this._apiKeyName = options?.config?.apiKeyName ?? TenantProcessor.DEFAULT_API_KEY_NAME;
|
|
61
|
-
this._tenantTokenName =
|
|
62
|
-
options?.config?.tenantTokenName ?? TenantUrlHelper.DEFAULT_TENANT_TOKEN_NAME;
|
|
63
|
-
// Vault is resolved only when a connector type is explicitly passed, following
|
|
64
|
-
if (Is.stringValue(options?.vaultConnectorType)) {
|
|
65
|
-
this._vaultConnector = VaultConnectorFactory.get(options.vaultConnectorType);
|
|
66
|
-
}
|
|
67
|
-
if (Is.stringValue(options?.config?.signingKeyName)) {
|
|
68
|
-
this._signingKeyName = options.config.signingKeyName;
|
|
69
|
-
}
|
|
70
44
|
}
|
|
71
45
|
/**
|
|
72
46
|
* Returns the class name of the component.
|
|
@@ -75,21 +49,6 @@ export class TenantProcessor {
|
|
|
75
49
|
className() {
|
|
76
50
|
return TenantProcessor.CLASS_NAME;
|
|
77
51
|
}
|
|
78
|
-
/**
|
|
79
|
-
* The processor needs to be started when the application is initialized so that
|
|
80
|
-
* the node identity is available for vault key resolution. Only required when
|
|
81
|
-
* the encrypted-token path is wired (i.e. `signingKeyName` is configured).
|
|
82
|
-
* @param nodeLoggingComponentType The node logging component type.
|
|
83
|
-
* @returns Nothing.
|
|
84
|
-
*/
|
|
85
|
-
async start(nodeLoggingComponentType) {
|
|
86
|
-
if (Is.empty(this._vaultConnector) || Is.empty(this._signingKeyName)) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const contextIds = await ContextIdStore.getContextIds();
|
|
90
|
-
ContextIdHelper.guard(contextIds, ContextIdKeys.Node);
|
|
91
|
-
this._nodeId = contextIds[ContextIdKeys.Node];
|
|
92
|
-
}
|
|
93
52
|
/**
|
|
94
53
|
* Pre process the REST request for the specified route.
|
|
95
54
|
* @param request The incoming request.
|
|
@@ -99,96 +58,66 @@ export class TenantProcessor {
|
|
|
99
58
|
* @param processorState The state handed through the processors.
|
|
100
59
|
*/
|
|
101
60
|
async pre(request, response, route, contextIds, processorState) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
61
|
+
if (!Is.empty(route) && !(route.skipTenant ?? false)) {
|
|
62
|
+
try {
|
|
63
|
+
const apiKey = request.headers?.[this._apiKeyName] ?? request.query?.[this._apiKeyName];
|
|
64
|
+
let tenant;
|
|
65
|
+
// First check apiKey as this is the most common way to authenticate tenants,
|
|
66
|
+
// if that is not present check for the tenant token query param which is used
|
|
67
|
+
// in scenarios where there is an embedded static token.
|
|
68
|
+
if (Is.stringValue(apiKey)) {
|
|
69
|
+
tenant = await this.resolveByApiKey(apiKey);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const tenantToken = await this._hostingComponent.getTenantTokenFromQueryParams(request.query);
|
|
73
|
+
if (Is.stringValue(tenantToken)) {
|
|
74
|
+
tenant = await this.resolveByTenantToken(tenantToken);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
throw new UnauthorizedError(TenantProcessor.CLASS_NAME, "missingApiKeyOrTenantToken", {
|
|
78
|
+
keyName: this._apiKeyName
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
contextIds[ContextIdKeys.Tenant] = tenant.id;
|
|
83
|
+
if (Is.stringValue(tenant.publicOrigin)) {
|
|
84
|
+
processorState.publicOrigin = tenant.publicOrigin;
|
|
113
85
|
}
|
|
114
86
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
let errorResponse;
|
|
119
|
-
if (Is.stringValue(apiKey)) {
|
|
120
|
-
errorResponse = await this.resolveByApiKey(apiKey, contextIds, processorState);
|
|
121
|
-
}
|
|
122
|
-
else if (tokenDecodable) {
|
|
123
|
-
errorResponse = await this.resolveByTenantToken(tenantToken, contextIds, processorState);
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
errorResponse = new UnauthorizedError(TenantProcessor.CLASS_NAME, "missingApiKey", {
|
|
127
|
-
keyName: this._apiKeyName
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
if (!Is.empty(errorResponse)) {
|
|
131
|
-
HttpErrorHelper.buildResponse(response, errorResponse, HttpStatusCode.unauthorized);
|
|
87
|
+
catch (err) {
|
|
88
|
+
HttpErrorHelper.buildResponse(response, BaseError.fromError(err), HttpStatusCode.unauthorized);
|
|
89
|
+
}
|
|
132
90
|
}
|
|
133
91
|
}
|
|
134
92
|
/**
|
|
135
93
|
* Resolve the tenant context from an api key.
|
|
136
94
|
* @param apiKey The api key sent by the caller.
|
|
137
|
-
* @
|
|
138
|
-
* @param processorState The state handed through the processors.
|
|
139
|
-
* @returns An error to surface, or undefined on success.
|
|
95
|
+
* @returns The tenant associated with the api key.
|
|
140
96
|
* @internal
|
|
141
97
|
*/
|
|
142
|
-
async resolveByApiKey(apiKey
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
contextIds[ContextIdKeys.Tenant] = nodeTenant.id;
|
|
151
|
-
if (Is.stringValue(nodeTenant.publicOrigin)) {
|
|
152
|
-
processorState.publicOrigin = nodeTenant.publicOrigin;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
return BaseError.fromError(err);
|
|
98
|
+
async resolveByApiKey(apiKey) {
|
|
99
|
+
const nodeTenant = await this._entityStorageConnector.get(apiKey, "apiKey");
|
|
100
|
+
if (Is.empty(nodeTenant)) {
|
|
101
|
+
throw new UnauthorizedError(TenantProcessor.CLASS_NAME, "apiKeyNotFound", {
|
|
102
|
+
key: apiKey
|
|
103
|
+
});
|
|
157
104
|
}
|
|
105
|
+
return nodeTenant;
|
|
158
106
|
}
|
|
159
107
|
/**
|
|
160
108
|
* Resolve the tenant context from an encrypted tenant token query param.
|
|
161
|
-
* @param
|
|
162
|
-
* @
|
|
163
|
-
* @param processorState The state handed through the processors.
|
|
164
|
-
* @returns An error to surface, or undefined on success.
|
|
109
|
+
* @param tenantId The encrypted tenant token.
|
|
110
|
+
* @returns The tenant associated with the tenant token.
|
|
165
111
|
* @internal
|
|
166
112
|
*/
|
|
167
|
-
async resolveByTenantToken(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
catch {
|
|
173
|
-
return new UnauthorizedError(TenantProcessor.CLASS_NAME, "tenantTokenInvalid", {
|
|
174
|
-
token: tenantToken
|
|
113
|
+
async resolveByTenantToken(tenantId) {
|
|
114
|
+
const nodeTenant = await this._entityStorageConnector.get(tenantId);
|
|
115
|
+
if (Is.empty(nodeTenant)) {
|
|
116
|
+
throw new UnauthorizedError(TenantProcessor.CLASS_NAME, "tenantNotFound", {
|
|
117
|
+
tenantId
|
|
175
118
|
});
|
|
176
119
|
}
|
|
177
|
-
|
|
178
|
-
const nodeTenant = await this._entityStorageConnector.get(tenantId);
|
|
179
|
-
if (Is.empty(nodeTenant)) {
|
|
180
|
-
return new UnauthorizedError(TenantProcessor.CLASS_NAME, "tenantTokenNotFound", {
|
|
181
|
-
tenantId
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
contextIds[ContextIdKeys.Tenant] = nodeTenant.id;
|
|
185
|
-
if (Is.stringValue(nodeTenant.publicOrigin)) {
|
|
186
|
-
processorState.publicOrigin = nodeTenant.publicOrigin;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
catch (err) {
|
|
190
|
-
return BaseError.fromError(err);
|
|
191
|
-
}
|
|
120
|
+
return nodeTenant;
|
|
192
121
|
}
|
|
193
122
|
}
|
|
194
123
|
//# sourceMappingURL=tenantProcessor.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenantProcessor.js","sourceRoot":"","sources":["../../src/tenantProcessor.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2025 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tHttpErrorHelper,\n\ttype IBaseRoute,\n\ttype IBaseRouteProcessor,\n\ttype IHttpResponse,\n\ttype IHttpServerRequest\n} from \"@twin.org/api-models\";\nimport {\n\tContextIdHelper,\n\tContextIdKeys,\n\tContextIdStore,\n\ttype IContextIds\n} from \"@twin.org/context\";\nimport { BaseError, type IError, Is, UnauthorizedError } from \"@twin.org/core\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { type IVaultConnector, VaultConnectorFactory } from \"@twin.org/vault-models\";\nimport { HttpStatusCode } from \"@twin.org/web\";\nimport type { Tenant } from \"./entities/tenant.js\";\nimport type { ITenantProcessorConstructorOptions } from \"./models/ITenantProcessorConstructorOptions.js\";\nimport { TenantUrlHelper } from \"./utils/tenantUrlHelper.js\";\n\n/**\n * Handles incoming api keys and maps them to tenant ids.\n */\nexport class TenantProcessor implements IBaseRouteProcessor {\n\t/**\n\t * The default name for the api key header.\n\t * @internal\n\t */\n\tpublic static readonly DEFAULT_API_KEY_NAME: string = \"x-api-key\";\n\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<TenantProcessor>();\n\n\t/**\n\t * The entity storage for api keys.\n\t * @internal\n\t */\n\tprivate readonly _entityStorageConnector: IEntityStorageConnector<Tenant>;\n\n\t/**\n\t * The key in the header to look for the api key.\n\t * @internal\n\t */\n\tprivate readonly _apiKeyName: string;\n\n\t/**\n\t * The query param name carrying the encrypted tenant token.\n\t * @internal\n\t */\n\tprivate readonly _tenantTokenName: string;\n\n\t/**\n\t * The vault connector used to decrypt tenant tokens.\n\t * Only set when `signingKeyName` is configured.\n\t * @internal\n\t */\n\tprivate readonly _vaultConnector?: IVaultConnector;\n\n\t/**\n\t * The name of the symmetric key in the vault used to decrypt tenant tokens.\n\t * @internal\n\t */\n\tprivate readonly _signingKeyName?: string;\n\n\t/**\n\t * The node identity, captured at start.\n\t * @internal\n\t */\n\tprivate _nodeId?: string;\n\n\t/**\n\t * Create a new instance of NodeTenantProcessor.\n\t * @param options Options for the processor.\n\t */\n\tconstructor(options?: ITenantProcessorConstructorOptions) {\n\t\tthis._entityStorageConnector = EntityStorageConnectorFactory.get(\n\t\t\toptions?.tenantEntityStorageType ?? \"tenant\"\n\t\t);\n\t\tthis._apiKeyName = options?.config?.apiKeyName ?? TenantProcessor.DEFAULT_API_KEY_NAME;\n\t\tthis._tenantTokenName =\n\t\t\toptions?.config?.tenantTokenName ?? TenantUrlHelper.DEFAULT_TENANT_TOKEN_NAME;\n\n\t\t// Vault is resolved only when a connector type is explicitly passed, following\n\t\tif (Is.stringValue(options?.vaultConnectorType)) {\n\t\t\tthis._vaultConnector = VaultConnectorFactory.get(options.vaultConnectorType);\n\t\t}\n\t\tif (Is.stringValue(options?.config?.signingKeyName)) {\n\t\t\tthis._signingKeyName = options.config.signingKeyName;\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 TenantProcessor.CLASS_NAME;\n\t}\n\n\t/**\n\t * The processor needs to be started when the application is initialized so that\n\t * the node identity is available for vault key resolution. Only required when\n\t * the encrypted-token path is wired (i.e. `signingKeyName` is configured).\n\t * @param nodeLoggingComponentType The node logging component type.\n\t * @returns Nothing.\n\t */\n\tpublic async start(nodeLoggingComponentType?: string): Promise<void> {\n\t\tif (Is.empty(this._vaultConnector) || Is.empty(this._signingKeyName)) {\n\t\t\treturn;\n\t\t}\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tContextIdHelper.guard(contextIds, ContextIdKeys.Node);\n\t\tthis._nodeId = contextIds[ContextIdKeys.Node];\n\t}\n\n\t/**\n\t * Pre process the REST request for the specified route.\n\t * @param request The incoming request.\n\t * @param response The outgoing response.\n\t * @param route The route to process.\n\t * @param contextIds The context IDs of the request.\n\t * @param processorState The state handed through the processors.\n\t */\n\tpublic async pre(\n\t\trequest: IHttpServerRequest,\n\t\tresponse: IHttpResponse,\n\t\troute: IBaseRoute | undefined,\n\t\tcontextIds: IContextIds,\n\t\tprocessorState: { [id: string]: unknown }\n\t): Promise<void> {\n\t\tconst tenantToken = request.query?.[this._tenantTokenName];\n\t\tconst tokenDecodable =\n\t\t\tIs.stringValue(tenantToken) &&\n\t\t\t!Is.empty(this._vaultConnector) &&\n\t\t\tIs.stringValue(this._signingKeyName);\n\n\t\t// skipTenant routes (cross-node trust JWT routes) bypass api-key resolution\n\t\t// accept an encrypted ?tenantToken= query param so cross-tenant catalogue/DSP/PNP routing reaches the publisher's tenant context\n\t\tif (Is.empty(route) || (route.skipTenant ?? false)) {\n\t\t\tif (tokenDecodable) {\n\t\t\t\tconst errorResponse = await this.resolveByTenantToken(\n\t\t\t\t\ttenantToken,\n\t\t\t\t\tcontextIds,\n\t\t\t\t\tprocessorState\n\t\t\t\t);\n\t\t\t\tif (!Is.empty(errorResponse)) {\n\t\t\t\t\tHttpErrorHelper.buildResponse(response, errorResponse, HttpStatusCode.unauthorized);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst apiKey = request.headers?.[this._apiKeyName] ?? request.query?.[this._apiKeyName];\n\t\tlet errorResponse: IError | undefined;\n\n\t\tif (Is.stringValue(apiKey)) {\n\t\t\terrorResponse = await this.resolveByApiKey(apiKey, contextIds, processorState);\n\t\t} else if (tokenDecodable) {\n\t\t\terrorResponse = await this.resolveByTenantToken(tenantToken, contextIds, processorState);\n\t\t} else {\n\t\t\terrorResponse = new UnauthorizedError(TenantProcessor.CLASS_NAME, \"missingApiKey\", {\n\t\t\t\tkeyName: this._apiKeyName\n\t\t\t});\n\t\t}\n\n\t\tif (!Is.empty(errorResponse)) {\n\t\t\tHttpErrorHelper.buildResponse(response, errorResponse, HttpStatusCode.unauthorized);\n\t\t}\n\t}\n\n\t/**\n\t * Resolve the tenant context from an api key.\n\t * @param apiKey The api key sent by the caller.\n\t * @param contextIds The context IDs of the request.\n\t * @param processorState The state handed through the processors.\n\t * @returns An error to surface, or undefined on success.\n\t * @internal\n\t */\n\tprivate async resolveByApiKey(\n\t\tapiKey: string,\n\t\tcontextIds: IContextIds,\n\t\tprocessorState: { [id: string]: unknown }\n\t): Promise<IError | undefined> {\n\t\ttry {\n\t\t\tconst nodeTenant = await this._entityStorageConnector.get(apiKey, \"apiKey\");\n\n\t\t\tif (Is.empty(nodeTenant)) {\n\t\t\t\treturn new UnauthorizedError(TenantProcessor.CLASS_NAME, \"apiKeyNotFound\", {\n\t\t\t\t\tkey: apiKey\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tcontextIds[ContextIdKeys.Tenant] = nodeTenant.id;\n\t\t\tif (Is.stringValue(nodeTenant.publicOrigin)) {\n\t\t\t\tprocessorState.publicOrigin = nodeTenant.publicOrigin;\n\t\t\t}\n\t\t} catch (err) {\n\t\t\treturn BaseError.fromError(err);\n\t\t}\n\t}\n\n\t/**\n\t * Resolve the tenant context from an encrypted tenant token query param.\n\t * @param tenantToken The encrypted tenant token.\n\t * @param contextIds The context IDs of the request.\n\t * @param processorState The state handed through the processors.\n\t * @returns An error to surface, or undefined on success.\n\t * @internal\n\t */\n\tprivate async resolveByTenantToken(\n\t\ttenantToken: string,\n\t\tcontextIds: IContextIds,\n\t\tprocessorState: { [id: string]: unknown }\n\t): Promise<IError | undefined> {\n\t\tlet tenantId: string;\n\t\ttry {\n\t\t\ttenantId = await TenantUrlHelper.decrypt(\n\t\t\t\ttenantToken,\n\t\t\t\tthis._vaultConnector as IVaultConnector,\n\t\t\t\t`${this._nodeId}/${this._signingKeyName}`\n\t\t\t);\n\t\t} catch {\n\t\t\treturn new UnauthorizedError(TenantProcessor.CLASS_NAME, \"tenantTokenInvalid\", {\n\t\t\t\ttoken: tenantToken\n\t\t\t});\n\t\t}\n\n\t\ttry {\n\t\t\tconst nodeTenant = await this._entityStorageConnector.get(tenantId);\n\n\t\t\tif (Is.empty(nodeTenant)) {\n\t\t\t\treturn new UnauthorizedError(TenantProcessor.CLASS_NAME, \"tenantTokenNotFound\", {\n\t\t\t\t\ttenantId\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tcontextIds[ContextIdKeys.Tenant] = nodeTenant.id;\n\t\t\tif (Is.stringValue(nodeTenant.publicOrigin)) {\n\t\t\t\tprocessorState.publicOrigin = nodeTenant.publicOrigin;\n\t\t\t}\n\t\t} catch (err) {\n\t\t\treturn BaseError.fromError(err);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tenantProcessor.js","sourceRoot":"","sources":["../../src/tenantProcessor.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2025 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tHttpErrorHelper,\n\ttype IBaseRoute,\n\ttype IBaseRouteProcessor,\n\ttype IHostingComponent,\n\ttype IHttpResponse,\n\ttype IHttpServerRequest\n} from \"@twin.org/api-models\";\nimport { ContextIdKeys, type IContextIds } from \"@twin.org/context\";\nimport { BaseError, ComponentFactory, Is, UnauthorizedError } from \"@twin.org/core\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { HttpStatusCode } from \"@twin.org/web\";\nimport type { Tenant } from \"./entities/tenant.js\";\nimport type { ITenantProcessorConstructorOptions } from \"./models/ITenantProcessorConstructorOptions.js\";\n\n/**\n * Handles incoming api keys and maps them to tenant ids.\n */\nexport class TenantProcessor implements IBaseRouteProcessor {\n\t/**\n\t * The default name for the api key header.\n\t * @internal\n\t */\n\tpublic static readonly DEFAULT_API_KEY_NAME: string = \"x-api-key\";\n\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<TenantProcessor>();\n\n\t/**\n\t * The entity storage for api keys.\n\t * @internal\n\t */\n\tprivate readonly _entityStorageConnector: IEntityStorageConnector<Tenant>;\n\n\t/**\n\t * The hosting component, used to resolve public origins for tenants and encrypt/decrypt tenant tokens.\n\t * @internal\n\t */\n\tprivate readonly _hostingComponent: IHostingComponent;\n\n\t/**\n\t * The key in the header to look for the api key.\n\t * @internal\n\t */\n\tprivate readonly _apiKeyName: string;\n\n\t/**\n\t * Create a new instance of TenantProcessor.\n\t * @param options Options for the processor.\n\t */\n\tconstructor(options?: ITenantProcessorConstructorOptions) {\n\t\tthis._entityStorageConnector = EntityStorageConnectorFactory.get(\n\t\t\toptions?.tenantEntityStorageType ?? \"tenant\"\n\t\t);\n\t\tthis._hostingComponent = ComponentFactory.get(options?.hostingComponentType ?? \"hosting\");\n\t\tthis._apiKeyName = options?.config?.apiKeyName ?? TenantProcessor.DEFAULT_API_KEY_NAME;\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 TenantProcessor.CLASS_NAME;\n\t}\n\n\t/**\n\t * Pre process the REST request for the specified route.\n\t * @param request The incoming request.\n\t * @param response The outgoing response.\n\t * @param route The route to process.\n\t * @param contextIds The context IDs of the request.\n\t * @param processorState The state handed through the processors.\n\t */\n\tpublic async pre(\n\t\trequest: IHttpServerRequest,\n\t\tresponse: IHttpResponse,\n\t\troute: IBaseRoute | undefined,\n\t\tcontextIds: IContextIds,\n\t\tprocessorState: { [id: string]: unknown }\n\t): Promise<void> {\n\t\tif (!Is.empty(route) && !(route.skipTenant ?? false)) {\n\t\t\ttry {\n\t\t\t\tconst apiKey = request.headers?.[this._apiKeyName] ?? request.query?.[this._apiKeyName];\n\t\t\t\tlet tenant;\n\n\t\t\t\t// First check apiKey as this is the most common way to authenticate tenants,\n\t\t\t\t// if that is not present check for the tenant token query param which is used\n\t\t\t\t// in scenarios where there is an embedded static token.\n\t\t\t\tif (Is.stringValue(apiKey)) {\n\t\t\t\t\ttenant = await this.resolveByApiKey(apiKey);\n\t\t\t\t} else {\n\t\t\t\t\tconst tenantToken = await this._hostingComponent.getTenantTokenFromQueryParams(\n\t\t\t\t\t\trequest.query\n\t\t\t\t\t);\n\n\t\t\t\t\tif (Is.stringValue(tenantToken)) {\n\t\t\t\t\t\ttenant = await this.resolveByTenantToken(tenantToken);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthrow new UnauthorizedError(TenantProcessor.CLASS_NAME, \"missingApiKeyOrTenantToken\", {\n\t\t\t\t\t\t\tkeyName: this._apiKeyName\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcontextIds[ContextIdKeys.Tenant] = tenant.id;\n\t\t\t\tif (Is.stringValue(tenant.publicOrigin)) {\n\t\t\t\t\tprocessorState.publicOrigin = tenant.publicOrigin;\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tHttpErrorHelper.buildResponse(\n\t\t\t\t\tresponse,\n\t\t\t\t\tBaseError.fromError(err),\n\t\t\t\t\tHttpStatusCode.unauthorized\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Resolve the tenant context from an api key.\n\t * @param apiKey The api key sent by the caller.\n\t * @returns The tenant associated with the api key.\n\t * @internal\n\t */\n\tprivate async resolveByApiKey(apiKey: string): Promise<Tenant> {\n\t\tconst nodeTenant = await this._entityStorageConnector.get(apiKey, \"apiKey\");\n\n\t\tif (Is.empty(nodeTenant)) {\n\t\t\tthrow new UnauthorizedError(TenantProcessor.CLASS_NAME, \"apiKeyNotFound\", {\n\t\t\t\tkey: apiKey\n\t\t\t});\n\t\t}\n\n\t\treturn nodeTenant;\n\t}\n\n\t/**\n\t * Resolve the tenant context from an encrypted tenant token query param.\n\t * @param tenantId The encrypted tenant token.\n\t * @returns The tenant associated with the tenant token.\n\t * @internal\n\t */\n\tprivate async resolveByTenantToken(tenantId: string): Promise<Tenant> {\n\t\tconst nodeTenant = await this._entityStorageConnector.get(tenantId);\n\n\t\tif (Is.empty(nodeTenant)) {\n\t\t\tthrow new UnauthorizedError(TenantProcessor.CLASS_NAME, \"tenantNotFound\", {\n\t\t\t\ttenantId\n\t\t\t});\n\t\t}\n\n\t\treturn nodeTenant;\n\t}\n}\n"]}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -7,14 +7,4 @@ export interface ITenantProcessorConfig {
|
|
|
7
7
|
* @default x-api-key
|
|
8
8
|
*/
|
|
9
9
|
apiKeyName?: string;
|
|
10
|
-
/**
|
|
11
|
-
* The name of the symmetric key in the vault used to encrypt/decrypt tenant tokens.
|
|
12
|
-
* @default tenant-token-encryption
|
|
13
|
-
*/
|
|
14
|
-
signingKeyName?: string;
|
|
15
|
-
/**
|
|
16
|
-
* The query param name to look for the encrypted tenant token.
|
|
17
|
-
* @default tenantToken
|
|
18
|
-
*/
|
|
19
|
-
tenantTokenName?: string;
|
|
20
10
|
}
|
|
@@ -9,11 +9,10 @@ export interface ITenantProcessorConstructorOptions {
|
|
|
9
9
|
*/
|
|
10
10
|
tenantEntityStorageType?: string;
|
|
11
11
|
/**
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
* @default vault
|
|
12
|
+
* The hosting component for the tenants.
|
|
13
|
+
* @default hosting
|
|
15
14
|
*/
|
|
16
|
-
|
|
15
|
+
hostingComponentType?: string;
|
|
17
16
|
/**
|
|
18
17
|
* Configuration for the processor.
|
|
19
18
|
*/
|
|
@@ -10,7 +10,7 @@ export declare class TenantProcessor implements IBaseRouteProcessor {
|
|
|
10
10
|
*/
|
|
11
11
|
static readonly CLASS_NAME: string;
|
|
12
12
|
/**
|
|
13
|
-
* Create a new instance of
|
|
13
|
+
* Create a new instance of TenantProcessor.
|
|
14
14
|
* @param options Options for the processor.
|
|
15
15
|
*/
|
|
16
16
|
constructor(options?: ITenantProcessorConstructorOptions);
|
|
@@ -19,14 +19,6 @@ export declare class TenantProcessor implements IBaseRouteProcessor {
|
|
|
19
19
|
* @returns The class name of the component.
|
|
20
20
|
*/
|
|
21
21
|
className(): string;
|
|
22
|
-
/**
|
|
23
|
-
* The processor needs to be started when the application is initialized so that
|
|
24
|
-
* the node identity is available for vault key resolution. Only required when
|
|
25
|
-
* the encrypted-token path is wired (i.e. `signingKeyName` is configured).
|
|
26
|
-
* @param nodeLoggingComponentType The node logging component type.
|
|
27
|
-
* @returns Nothing.
|
|
28
|
-
*/
|
|
29
|
-
start(nodeLoggingComponentType?: string): Promise<void>;
|
|
30
22
|
/**
|
|
31
23
|
* Pre process the REST request for the specified route.
|
|
32
24
|
* @param request The incoming request.
|
package/docs/changelog.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.3-next.29](https://github.com/twinfoundation/twin-api/compare/api-tenant-processor-v0.0.3-next.28...api-tenant-processor-v0.0.3-next.29) (2026-05-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* hosting service ([#109](https://github.com/twinfoundation/twin-api/issues/109)) ([985bf1f](https://github.com/twinfoundation/twin-api/commit/985bf1f5c07b09ecb800df7120bc2422ac7a6d25))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @twin.org/api-models bumped from 0.0.3-next.28 to 0.0.3-next.29
|
|
16
|
+
|
|
3
17
|
## [0.0.3-next.28](https://github.com/twinfoundation/twin-api/compare/api-tenant-processor-v0.0.3-next.27...api-tenant-processor-v0.0.3-next.28) (2026-04-30)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -12,7 +12,7 @@ Handles incoming api keys and maps them to tenant ids.
|
|
|
12
12
|
|
|
13
13
|
> **new TenantProcessor**(`options?`): `TenantProcessor`
|
|
14
14
|
|
|
15
|
-
Create a new instance of
|
|
15
|
+
Create a new instance of TenantProcessor.
|
|
16
16
|
|
|
17
17
|
#### Parameters
|
|
18
18
|
|
|
@@ -54,34 +54,6 @@ The class name of the component.
|
|
|
54
54
|
|
|
55
55
|
***
|
|
56
56
|
|
|
57
|
-
### start() {#start}
|
|
58
|
-
|
|
59
|
-
> **start**(`nodeLoggingComponentType?`): `Promise`\<`void`\>
|
|
60
|
-
|
|
61
|
-
The processor needs to be started when the application is initialized so that
|
|
62
|
-
the node identity is available for vault key resolution. Only required when
|
|
63
|
-
the encrypted-token path is wired (i.e. `signingKeyName` is configured).
|
|
64
|
-
|
|
65
|
-
#### Parameters
|
|
66
|
-
|
|
67
|
-
##### nodeLoggingComponentType?
|
|
68
|
-
|
|
69
|
-
`string`
|
|
70
|
-
|
|
71
|
-
The node logging component type.
|
|
72
|
-
|
|
73
|
-
#### Returns
|
|
74
|
-
|
|
75
|
-
`Promise`\<`void`\>
|
|
76
|
-
|
|
77
|
-
Nothing.
|
|
78
|
-
|
|
79
|
-
#### Implementation of
|
|
80
|
-
|
|
81
|
-
`IBaseRouteProcessor.start`
|
|
82
|
-
|
|
83
|
-
***
|
|
84
|
-
|
|
85
57
|
### pre() {#pre}
|
|
86
58
|
|
|
87
59
|
> **pre**(`request`, `response`, `route`, `contextIds`, `processorState`): `Promise`\<`void`\>
|
package/docs/reference/index.md
CHANGED
|
@@ -15,31 +15,3 @@ The key to look for in the header or query params for the api key.
|
|
|
15
15
|
```ts
|
|
16
16
|
x-api-key
|
|
17
17
|
```
|
|
18
|
-
|
|
19
|
-
***
|
|
20
|
-
|
|
21
|
-
### signingKeyName? {#signingkeyname}
|
|
22
|
-
|
|
23
|
-
> `optional` **signingKeyName?**: `string`
|
|
24
|
-
|
|
25
|
-
The name of the symmetric key in the vault used to encrypt/decrypt tenant tokens.
|
|
26
|
-
|
|
27
|
-
#### Default
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
tenant-token-encryption
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
***
|
|
34
|
-
|
|
35
|
-
### tenantTokenName? {#tenanttokenname}
|
|
36
|
-
|
|
37
|
-
> `optional` **tenantTokenName?**: `string`
|
|
38
|
-
|
|
39
|
-
The query param name to look for the encrypted tenant token.
|
|
40
|
-
|
|
41
|
-
#### Default
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
tenantToken
|
|
45
|
-
```
|
|
@@ -18,17 +18,16 @@ tenant
|
|
|
18
18
|
|
|
19
19
|
***
|
|
20
20
|
|
|
21
|
-
###
|
|
21
|
+
### hostingComponentType? {#hostingcomponenttype}
|
|
22
22
|
|
|
23
|
-
> `optional` **
|
|
23
|
+
> `optional` **hostingComponentType?**: `string`
|
|
24
24
|
|
|
25
|
-
The
|
|
26
|
-
`config.signingKeyName` is set.
|
|
25
|
+
The hosting component for the tenants.
|
|
27
26
|
|
|
28
27
|
#### Default
|
|
29
28
|
|
|
30
29
|
```ts
|
|
31
|
-
|
|
30
|
+
hosting
|
|
32
31
|
```
|
|
33
32
|
|
|
34
33
|
***
|
package/locales/en.json
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"error": {
|
|
3
3
|
"tenantProcessor": {
|
|
4
|
-
"
|
|
4
|
+
"missingApiKeyOrTenantToken": "API key \"{keyName}\" or tenant token is missing",
|
|
5
5
|
"apiKeyNotFound": "No node tenant found for API key \"{key}\".",
|
|
6
|
-
"
|
|
7
|
-
"tenantTokenNotFound": "No node tenant found for the decrypted tenant token \"{tenantId}\"."
|
|
8
|
-
},
|
|
9
|
-
"tenantUrlHelper": {
|
|
10
|
-
"encryptFailed": "Encryption of tenant URL token failed.",
|
|
11
|
-
"decryptFailed": "Decryption of tenant URL token failed."
|
|
6
|
+
"tenantNotFound": "No node tenant found for the decrypted tenant token \"{tenantId}\"."
|
|
12
7
|
},
|
|
13
8
|
"tenantAdminService": {
|
|
14
9
|
"apiKeyAlreadyInUse": "The provided API key is already in use by another tenant.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twin.org/api-tenant-processor",
|
|
3
|
-
"version": "0.0.3-next.
|
|
3
|
+
"version": "0.0.3-next.29",
|
|
4
4
|
"description": "Tenant resolution services and route handlers that derive tenant context from API keys.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"node": ">=20.0.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@twin.org/api-models": "0.0.3-next.
|
|
17
|
+
"@twin.org/api-models": "0.0.3-next.29",
|
|
18
18
|
"@twin.org/context": "next",
|
|
19
19
|
"@twin.org/core": "next",
|
|
20
20
|
"@twin.org/entity": "next",
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
// Copyright 2025 IOTA Stiftung.
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
-
import { Converter, GeneralError, Guards } from "@twin.org/core";
|
|
4
|
-
import { VaultEncryptionType } from "@twin.org/vault-models";
|
|
5
|
-
/**
|
|
6
|
-
* Helper for building and parsing URLs that carry an encrypted tenant token query param.
|
|
7
|
-
* The token is the ChaCha20Poly1305-encrypted UTF-8 bytes of a tenant id, base64url-encoded
|
|
8
|
-
* for URL safety.
|
|
9
|
-
*/
|
|
10
|
-
export class TenantUrlHelper {
|
|
11
|
-
/**
|
|
12
|
-
* The default query param name for the encrypted tenant token.
|
|
13
|
-
*/
|
|
14
|
-
static DEFAULT_TENANT_TOKEN_NAME = "tenantToken";
|
|
15
|
-
/**
|
|
16
|
-
* Runtime name for the class.
|
|
17
|
-
*/
|
|
18
|
-
static CLASS_NAME = "TenantUrlHelper";
|
|
19
|
-
/**
|
|
20
|
-
* Encrypt a tenant id and append it as an opaque query param to the supplied URL.
|
|
21
|
-
* @param url The URL to append the token to.
|
|
22
|
-
* @param tenantId The tenant id to encrypt into the token.
|
|
23
|
-
* @param vaultConnector The vault connector providing the symmetric key.
|
|
24
|
-
* @param keyName The fully-qualified vault key name (e.g. `${nodeId}/tenant-token-encryption`).
|
|
25
|
-
* @param tenantTokenName The query param name. Defaults to `tenantToken`.
|
|
26
|
-
* @returns The URL with the encrypted tenant token appended.
|
|
27
|
-
*/
|
|
28
|
-
static async encrypt(url, tenantId, vaultConnector, keyName, tenantTokenName) {
|
|
29
|
-
Guards.stringValue(TenantUrlHelper.CLASS_NAME, "url", url);
|
|
30
|
-
Guards.stringValue(TenantUrlHelper.CLASS_NAME, "tenantId", tenantId);
|
|
31
|
-
Guards.object(TenantUrlHelper.CLASS_NAME, "vaultConnector", vaultConnector);
|
|
32
|
-
Guards.stringValue(TenantUrlHelper.CLASS_NAME, "keyName", keyName);
|
|
33
|
-
try {
|
|
34
|
-
const encrypted = await vaultConnector.encrypt(keyName, VaultEncryptionType.ChaCha20Poly1305, Converter.utf8ToBytes(tenantId));
|
|
35
|
-
const token = Converter.bytesToBase64Url(encrypted);
|
|
36
|
-
const paramName = tenantTokenName ?? TenantUrlHelper.DEFAULT_TENANT_TOKEN_NAME;
|
|
37
|
-
const separator = url.includes("?") ? "&" : "?";
|
|
38
|
-
return `${url}${separator}${paramName}=${token}`;
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
throw new GeneralError(TenantUrlHelper.CLASS_NAME, "encryptFailed", undefined, err);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Decrypt an encrypted tenant token back into the original tenant id.
|
|
46
|
-
* @param token The base64url-encoded encrypted tenant token.
|
|
47
|
-
* @param vaultConnector The vault connector providing the symmetric key.
|
|
48
|
-
* @param keyName The fully-qualified vault key name used to encrypt the token.
|
|
49
|
-
* @returns The decrypted tenant id.
|
|
50
|
-
*/
|
|
51
|
-
static async decrypt(token, vaultConnector, keyName) {
|
|
52
|
-
Guards.stringValue(TenantUrlHelper.CLASS_NAME, "token", token);
|
|
53
|
-
Guards.object(TenantUrlHelper.CLASS_NAME, "vaultConnector", vaultConnector);
|
|
54
|
-
Guards.stringValue(TenantUrlHelper.CLASS_NAME, "keyName", keyName);
|
|
55
|
-
try {
|
|
56
|
-
const encrypted = Converter.base64UrlToBytes(token);
|
|
57
|
-
const decrypted = await vaultConnector.decrypt(keyName, VaultEncryptionType.ChaCha20Poly1305, encrypted);
|
|
58
|
-
return Converter.bytesToUtf8(decrypted);
|
|
59
|
-
}
|
|
60
|
-
catch (err) {
|
|
61
|
-
throw new GeneralError(TenantUrlHelper.CLASS_NAME, "decryptFailed", undefined, err);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
//# sourceMappingURL=tenantUrlHelper.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tenantUrlHelper.js","sourceRoot":"","sources":["../../../src/utils/tenantUrlHelper.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAEjE,OAAO,EAAwB,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAEnF;;;;GAIG;AACH,MAAM,OAAO,eAAe;IAC3B;;OAEG;IACI,MAAM,CAAU,yBAAyB,GAAW,aAAa,CAAC;IAEzE;;OAEG;IACI,MAAM,CAAU,UAAU,qBAAqC;IAEtE;;;;;;;;OAQG;IACI,MAAM,CAAC,KAAK,CAAC,OAAO,CAC1B,GAAW,EACX,QAAgB,EAChB,cAA+B,EAC/B,OAAe,EACf,eAAwB;QAExB,MAAM,CAAC,WAAW,CAAC,eAAe,CAAC,UAAU,SAAe,GAAG,CAAC,CAAC;QACjE,MAAM,CAAC,WAAW,CAAC,eAAe,CAAC,UAAU,cAAoB,QAAQ,CAAC,CAAC;QAC3E,MAAM,CAAC,MAAM,CACZ,eAAe,CAAC,UAAU,oBAE1B,cAAc,CACd,CAAC;QACF,MAAM,CAAC,WAAW,CAAC,eAAe,CAAC,UAAU,aAAmB,OAAO,CAAC,CAAC;QAEzE,IAAI,CAAC;YACJ,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,OAAO,CAC7C,OAAO,EACP,mBAAmB,CAAC,gBAAgB,EACpC,SAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,CAC/B,CAAC;YACF,MAAM,KAAK,GAAG,SAAS,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;YACpD,MAAM,SAAS,GAAG,eAAe,IAAI,eAAe,CAAC,yBAAyB,CAAC;YAC/E,MAAM,SAAS,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;YAChD,OAAO,GAAG,GAAG,GAAG,SAAS,GAAG,SAAS,IAAI,KAAK,EAAE,CAAC;QAClD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,MAAM,IAAI,YAAY,CAAC,eAAe,CAAC,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;QACrF,CAAC;IACF,CAAC;IAED;;;;;;OAMG;IACI,MAAM,CAAC,KAAK,CAAC,OAAO,CAC1B,KAAa,EACb,cAA+B,EAC/B,OAAe;QAEf,MAAM,CAAC,WAAW,CAAC,eAAe,CAAC,UAAU,WAAiB,KAAK,CAAC,CAAC;QACrE,MAAM,CAAC,MAAM,CACZ,eAAe,CAAC,UAAU,oBAE1B,cAAc,CACd,CAAC;QACF,MAAM,CAAC,WAAW,CAAC,eAAe,CAAC,UAAU,aAAmB,OAAO,CAAC,CAAC;QAEzE,IAAI,CAAC;YACJ,MAAM,SAAS,GAAG,SAAS,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;YACpD,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,OAAO,CAC7C,OAAO,EACP,mBAAmB,CAAC,gBAAgB,EACpC,SAAS,CACT,CAAC;YACF,OAAO,SAAS,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QACzC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,MAAM,IAAI,YAAY,CAAC,eAAe,CAAC,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;QACrF,CAAC;IACF,CAAC","sourcesContent":["// Copyright 2025 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Converter, GeneralError, Guards } from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { type IVaultConnector, VaultEncryptionType } from \"@twin.org/vault-models\";\n\n/**\n * Helper for building and parsing URLs that carry an encrypted tenant token query param.\n * The token is the ChaCha20Poly1305-encrypted UTF-8 bytes of a tenant id, base64url-encoded\n * for URL safety.\n */\nexport class TenantUrlHelper {\n\t/**\n\t * The default query param name for the encrypted tenant token.\n\t */\n\tpublic static readonly DEFAULT_TENANT_TOKEN_NAME: string = \"tenantToken\";\n\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<TenantUrlHelper>();\n\n\t/**\n\t * Encrypt a tenant id and append it as an opaque query param to the supplied URL.\n\t * @param url The URL to append the token to.\n\t * @param tenantId The tenant id to encrypt into the token.\n\t * @param vaultConnector The vault connector providing the symmetric key.\n\t * @param keyName The fully-qualified vault key name (e.g. `${nodeId}/tenant-token-encryption`).\n\t * @param tenantTokenName The query param name. Defaults to `tenantToken`.\n\t * @returns The URL with the encrypted tenant token appended.\n\t */\n\tpublic static async encrypt(\n\t\turl: string,\n\t\ttenantId: string,\n\t\tvaultConnector: IVaultConnector,\n\t\tkeyName: string,\n\t\ttenantTokenName?: string\n\t): Promise<string> {\n\t\tGuards.stringValue(TenantUrlHelper.CLASS_NAME, nameof(url), url);\n\t\tGuards.stringValue(TenantUrlHelper.CLASS_NAME, nameof(tenantId), tenantId);\n\t\tGuards.object<IVaultConnector>(\n\t\t\tTenantUrlHelper.CLASS_NAME,\n\t\t\tnameof(vaultConnector),\n\t\t\tvaultConnector\n\t\t);\n\t\tGuards.stringValue(TenantUrlHelper.CLASS_NAME, nameof(keyName), keyName);\n\n\t\ttry {\n\t\t\tconst encrypted = await vaultConnector.encrypt(\n\t\t\t\tkeyName,\n\t\t\t\tVaultEncryptionType.ChaCha20Poly1305,\n\t\t\t\tConverter.utf8ToBytes(tenantId)\n\t\t\t);\n\t\t\tconst token = Converter.bytesToBase64Url(encrypted);\n\t\t\tconst paramName = tenantTokenName ?? TenantUrlHelper.DEFAULT_TENANT_TOKEN_NAME;\n\t\t\tconst separator = url.includes(\"?\") ? \"&\" : \"?\";\n\t\t\treturn `${url}${separator}${paramName}=${token}`;\n\t\t} catch (err) {\n\t\t\tthrow new GeneralError(TenantUrlHelper.CLASS_NAME, \"encryptFailed\", undefined, err);\n\t\t}\n\t}\n\n\t/**\n\t * Decrypt an encrypted tenant token back into the original tenant id.\n\t * @param token The base64url-encoded encrypted tenant token.\n\t * @param vaultConnector The vault connector providing the symmetric key.\n\t * @param keyName The fully-qualified vault key name used to encrypt the token.\n\t * @returns The decrypted tenant id.\n\t */\n\tpublic static async decrypt(\n\t\ttoken: string,\n\t\tvaultConnector: IVaultConnector,\n\t\tkeyName: string\n\t): Promise<string> {\n\t\tGuards.stringValue(TenantUrlHelper.CLASS_NAME, nameof(token), token);\n\t\tGuards.object<IVaultConnector>(\n\t\t\tTenantUrlHelper.CLASS_NAME,\n\t\t\tnameof(vaultConnector),\n\t\t\tvaultConnector\n\t\t);\n\t\tGuards.stringValue(TenantUrlHelper.CLASS_NAME, nameof(keyName), keyName);\n\n\t\ttry {\n\t\t\tconst encrypted = Converter.base64UrlToBytes(token);\n\t\t\tconst decrypted = await vaultConnector.decrypt(\n\t\t\t\tkeyName,\n\t\t\t\tVaultEncryptionType.ChaCha20Poly1305,\n\t\t\t\tencrypted\n\t\t\t);\n\t\t\treturn Converter.bytesToUtf8(decrypted);\n\t\t} catch (err) {\n\t\t\tthrow new GeneralError(TenantUrlHelper.CLASS_NAME, \"decryptFailed\", undefined, err);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { type IVaultConnector } from "@twin.org/vault-models";
|
|
2
|
-
/**
|
|
3
|
-
* Helper for building and parsing URLs that carry an encrypted tenant token query param.
|
|
4
|
-
* The token is the ChaCha20Poly1305-encrypted UTF-8 bytes of a tenant id, base64url-encoded
|
|
5
|
-
* for URL safety.
|
|
6
|
-
*/
|
|
7
|
-
export declare class TenantUrlHelper {
|
|
8
|
-
/**
|
|
9
|
-
* The default query param name for the encrypted tenant token.
|
|
10
|
-
*/
|
|
11
|
-
static readonly DEFAULT_TENANT_TOKEN_NAME: string;
|
|
12
|
-
/**
|
|
13
|
-
* Runtime name for the class.
|
|
14
|
-
*/
|
|
15
|
-
static readonly CLASS_NAME: string;
|
|
16
|
-
/**
|
|
17
|
-
* Encrypt a tenant id and append it as an opaque query param to the supplied URL.
|
|
18
|
-
* @param url The URL to append the token to.
|
|
19
|
-
* @param tenantId The tenant id to encrypt into the token.
|
|
20
|
-
* @param vaultConnector The vault connector providing the symmetric key.
|
|
21
|
-
* @param keyName The fully-qualified vault key name (e.g. `${nodeId}/tenant-token-encryption`).
|
|
22
|
-
* @param tenantTokenName The query param name. Defaults to `tenantToken`.
|
|
23
|
-
* @returns The URL with the encrypted tenant token appended.
|
|
24
|
-
*/
|
|
25
|
-
static encrypt(url: string, tenantId: string, vaultConnector: IVaultConnector, keyName: string, tenantTokenName?: string): Promise<string>;
|
|
26
|
-
/**
|
|
27
|
-
* Decrypt an encrypted tenant token back into the original tenant id.
|
|
28
|
-
* @param token The base64url-encoded encrypted tenant token.
|
|
29
|
-
* @param vaultConnector The vault connector providing the symmetric key.
|
|
30
|
-
* @param keyName The fully-qualified vault key name used to encrypt the token.
|
|
31
|
-
* @returns The decrypted tenant id.
|
|
32
|
-
*/
|
|
33
|
-
static decrypt(token: string, vaultConnector: IVaultConnector, keyName: string): Promise<string>;
|
|
34
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# Class: TenantUrlHelper
|
|
2
|
-
|
|
3
|
-
Helper for building and parsing URLs that carry an encrypted tenant token query param.
|
|
4
|
-
The token is the ChaCha20Poly1305-encrypted UTF-8 bytes of a tenant id, base64url-encoded
|
|
5
|
-
for URL safety.
|
|
6
|
-
|
|
7
|
-
## Constructors
|
|
8
|
-
|
|
9
|
-
### Constructor
|
|
10
|
-
|
|
11
|
-
> **new TenantUrlHelper**(): `TenantUrlHelper`
|
|
12
|
-
|
|
13
|
-
#### Returns
|
|
14
|
-
|
|
15
|
-
`TenantUrlHelper`
|
|
16
|
-
|
|
17
|
-
## Properties
|
|
18
|
-
|
|
19
|
-
### DEFAULT\_TENANT\_TOKEN\_NAME {#default_tenant_token_name}
|
|
20
|
-
|
|
21
|
-
> `readonly` `static` **DEFAULT\_TENANT\_TOKEN\_NAME**: `string` = `"tenantToken"`
|
|
22
|
-
|
|
23
|
-
The default query param name for the encrypted tenant token.
|
|
24
|
-
|
|
25
|
-
***
|
|
26
|
-
|
|
27
|
-
### CLASS\_NAME {#class_name}
|
|
28
|
-
|
|
29
|
-
> `readonly` `static` **CLASS\_NAME**: `string`
|
|
30
|
-
|
|
31
|
-
Runtime name for the class.
|
|
32
|
-
|
|
33
|
-
## Methods
|
|
34
|
-
|
|
35
|
-
### encrypt() {#encrypt}
|
|
36
|
-
|
|
37
|
-
> `static` **encrypt**(`url`, `tenantId`, `vaultConnector`, `keyName`, `tenantTokenName?`): `Promise`\<`string`\>
|
|
38
|
-
|
|
39
|
-
Encrypt a tenant id and append it as an opaque query param to the supplied URL.
|
|
40
|
-
|
|
41
|
-
#### Parameters
|
|
42
|
-
|
|
43
|
-
##### url
|
|
44
|
-
|
|
45
|
-
`string`
|
|
46
|
-
|
|
47
|
-
The URL to append the token to.
|
|
48
|
-
|
|
49
|
-
##### tenantId
|
|
50
|
-
|
|
51
|
-
`string`
|
|
52
|
-
|
|
53
|
-
The tenant id to encrypt into the token.
|
|
54
|
-
|
|
55
|
-
##### vaultConnector
|
|
56
|
-
|
|
57
|
-
`IVaultConnector`
|
|
58
|
-
|
|
59
|
-
The vault connector providing the symmetric key.
|
|
60
|
-
|
|
61
|
-
##### keyName
|
|
62
|
-
|
|
63
|
-
`string`
|
|
64
|
-
|
|
65
|
-
The fully-qualified vault key name (e.g. `${nodeId}/tenant-token-encryption`).
|
|
66
|
-
|
|
67
|
-
##### tenantTokenName?
|
|
68
|
-
|
|
69
|
-
`string`
|
|
70
|
-
|
|
71
|
-
The query param name. Defaults to `tenantToken`.
|
|
72
|
-
|
|
73
|
-
#### Returns
|
|
74
|
-
|
|
75
|
-
`Promise`\<`string`\>
|
|
76
|
-
|
|
77
|
-
The URL with the encrypted tenant token appended.
|
|
78
|
-
|
|
79
|
-
***
|
|
80
|
-
|
|
81
|
-
### decrypt() {#decrypt}
|
|
82
|
-
|
|
83
|
-
> `static` **decrypt**(`token`, `vaultConnector`, `keyName`): `Promise`\<`string`\>
|
|
84
|
-
|
|
85
|
-
Decrypt an encrypted tenant token back into the original tenant id.
|
|
86
|
-
|
|
87
|
-
#### Parameters
|
|
88
|
-
|
|
89
|
-
##### token
|
|
90
|
-
|
|
91
|
-
`string`
|
|
92
|
-
|
|
93
|
-
The base64url-encoded encrypted tenant token.
|
|
94
|
-
|
|
95
|
-
##### vaultConnector
|
|
96
|
-
|
|
97
|
-
`IVaultConnector`
|
|
98
|
-
|
|
99
|
-
The vault connector providing the symmetric key.
|
|
100
|
-
|
|
101
|
-
##### keyName
|
|
102
|
-
|
|
103
|
-
`string`
|
|
104
|
-
|
|
105
|
-
The fully-qualified vault key name used to encrypt the token.
|
|
106
|
-
|
|
107
|
-
#### Returns
|
|
108
|
-
|
|
109
|
-
`Promise`\<`string`\>
|
|
110
|
-
|
|
111
|
-
The decrypted tenant id.
|