@twin.org/api-service 0.0.3-next.29 → 0.0.3-next.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/es/healthRoutes.js +108 -0
  2. package/dist/es/healthRoutes.js.map +1 -0
  3. package/dist/es/healthService.js +136 -0
  4. package/dist/es/healthService.js.map +1 -0
  5. package/dist/es/hostingService.js +1 -209
  6. package/dist/es/hostingService.js.map +1 -1
  7. package/dist/es/index.js +7 -0
  8. package/dist/es/index.js.map +1 -1
  9. package/dist/es/informationRoutes.js +31 -65
  10. package/dist/es/informationRoutes.js.map +1 -1
  11. package/dist/es/informationService.js +9 -85
  12. package/dist/es/informationService.js.map +1 -1
  13. package/dist/es/models/IHealthServiceConfig.js +4 -0
  14. package/dist/es/models/IHealthServiceConfig.js.map +1 -0
  15. package/dist/es/models/IHealthServiceConstructorOptions.js +2 -0
  16. package/dist/es/models/IHealthServiceConstructorOptions.js.map +1 -0
  17. package/dist/es/models/IHostingServiceConfig.js.map +1 -1
  18. package/dist/es/models/IHostingServiceConstructorOptions.js.map +1 -1
  19. package/dist/es/models/IUrlTransformerServiceConfig.js +4 -0
  20. package/dist/es/models/IUrlTransformerServiceConfig.js.map +1 -0
  21. package/dist/es/models/IUrlTransformerServiceConstructorOptions.js +2 -0
  22. package/dist/es/models/IUrlTransformerServiceConstructorOptions.js.map +1 -0
  23. package/dist/es/restEntryPoints.js +7 -0
  24. package/dist/es/restEntryPoints.js.map +1 -1
  25. package/dist/es/urlTransformerService.js +214 -0
  26. package/dist/es/urlTransformerService.js.map +1 -0
  27. package/dist/types/healthRoutes.d.ts +20 -0
  28. package/dist/types/healthService.d.ts +42 -0
  29. package/dist/types/hostingService.d.ts +1 -62
  30. package/dist/types/index.d.ts +7 -0
  31. package/dist/types/informationRoutes.d.ts +3 -3
  32. package/dist/types/informationService.d.ts +9 -21
  33. package/dist/types/models/IHealthServiceConfig.d.ts +10 -0
  34. package/dist/types/models/IHealthServiceConstructorOptions.d.ts +10 -0
  35. package/dist/types/models/IHostingServiceConfig.d.ts +0 -10
  36. package/dist/types/models/IHostingServiceConstructorOptions.d.ts +1 -6
  37. package/dist/types/models/IUrlTransformerServiceConfig.d.ts +19 -0
  38. package/dist/types/models/IUrlTransformerServiceConstructorOptions.d.ts +15 -0
  39. package/dist/types/urlTransformerService.d.ts +81 -0
  40. package/docs/changelog.md +14 -0
  41. package/docs/reference/classes/HealthService.md +123 -0
  42. package/docs/reference/classes/HostingService.md +0 -266
  43. package/docs/reference/classes/InformationService.md +8 -84
  44. package/docs/reference/classes/UrlTransformerService.md +321 -0
  45. package/docs/reference/functions/generateRestRoutesHealth.md +25 -0
  46. package/docs/reference/functions/serverReadyz.md +31 -0
  47. package/docs/reference/index.md +10 -1
  48. package/docs/reference/interfaces/IHealthServiceConfig.md +17 -0
  49. package/docs/reference/interfaces/IHealthServiceConstructorOptions.md +11 -0
  50. package/docs/reference/interfaces/IHostingServiceConfig.md +0 -28
  51. package/docs/reference/interfaces/IHostingServiceConstructorOptions.md +1 -15
  52. package/docs/reference/interfaces/IUrlTransformerServiceConfig.md +32 -0
  53. package/docs/reference/interfaces/IUrlTransformerServiceConstructorOptions.md +25 -0
  54. package/docs/reference/variables/tagsHealth.md +5 -0
  55. package/locales/en.json +6 -3
  56. package/package.json +4 -2
@@ -0,0 +1,108 @@
1
+ import { ComponentFactory } from "@twin.org/core";
2
+ /**
3
+ * The tag to associate with the routes.
4
+ */
5
+ export const tagsHealth = [
6
+ {
7
+ name: "Health",
8
+ description: "Health endpoints for the REST server."
9
+ }
10
+ ];
11
+ /**
12
+ * The REST routes for server health.
13
+ * @param baseRouteName Prefix to prepend to the paths.
14
+ * @param componentName The name of the component to use in the routes stored in the ComponentFactory.
15
+ * @returns The generated routes.
16
+ */
17
+ export function generateRestRoutesHealth(baseRouteName, componentName) {
18
+ const healthRoute = {
19
+ operationId: "serverHealth",
20
+ summary: "Get the health for the server",
21
+ tag: tagsHealth[0].name,
22
+ method: "GET",
23
+ path: `${baseRouteName}/`,
24
+ handler: async (httpRequestContext, request) => serverHealth(httpRequestContext, componentName, request),
25
+ responseType: [
26
+ {
27
+ type: "IServerHealthResponse",
28
+ examples: [
29
+ {
30
+ id: "healthResponseOK",
31
+ description: "The response for the health request.",
32
+ response: {
33
+ body: {
34
+ status: "ok",
35
+ components: [
36
+ {
37
+ name: "Database",
38
+ status: "ok"
39
+ },
40
+ {
41
+ name: "Storage",
42
+ status: "ok"
43
+ }
44
+ ]
45
+ }
46
+ }
47
+ },
48
+ {
49
+ id: "healthResponseWarning",
50
+ description: "The response for the health request with warnings.",
51
+ response: {
52
+ body: {
53
+ status: "warning",
54
+ components: [
55
+ {
56
+ name: "Database",
57
+ status: "warning",
58
+ description: "slowRunning"
59
+ },
60
+ {
61
+ name: "Storage",
62
+ status: "ok"
63
+ }
64
+ ]
65
+ }
66
+ }
67
+ },
68
+ {
69
+ id: "healthResponseError",
70
+ description: "The response for the health request with errors.",
71
+ response: {
72
+ body: {
73
+ status: "error",
74
+ components: [
75
+ {
76
+ name: "Database",
77
+ status: "ok"
78
+ },
79
+ {
80
+ name: "Storage",
81
+ status: "error",
82
+ description: "storageFull"
83
+ }
84
+ ]
85
+ }
86
+ }
87
+ }
88
+ ]
89
+ }
90
+ ],
91
+ skipAuth: true
92
+ };
93
+ return [healthRoute];
94
+ }
95
+ /**
96
+ * Get the health for the server.
97
+ * @param httpRequestContext The request context for the API.
98
+ * @param componentName The name of the component to use in the routes.
99
+ * @param request The request.
100
+ * @returns The response object with additional http response properties.
101
+ */
102
+ export async function serverHealth(httpRequestContext, componentName, request) {
103
+ const component = ComponentFactory.get(componentName);
104
+ return {
105
+ body: await component.healthStatus()
106
+ };
107
+ }
108
+ //# sourceMappingURL=healthRoutes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healthRoutes.js","sourceRoot":"","sources":["../../src/healthRoutes.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGlD;;GAEG;AACH,MAAM,CAAC,MAAM,UAAU,GAAW;IACjC;QACC,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,uCAAuC;KACpD;CACD,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CACvC,aAAqB,EACrB,aAAqB;IAErB,MAAM,WAAW,GAAyD;QACzE,WAAW,EAAE,cAAc;QAC3B,OAAO,EAAE,+BAA+B;QACxC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI;QACvB,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,GAAG,aAAa,GAAG;QACzB,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,EAAE,CAC9C,YAAY,CAAC,kBAAkB,EAAE,aAAa,EAAE,OAAO,CAAC;QACzD,YAAY,EAAE;YACb;gBACC,IAAI,yBAAiC;gBACrC,QAAQ,EAAE;oBACT;wBACC,EAAE,EAAE,kBAAkB;wBACtB,WAAW,EAAE,sCAAsC;wBACnD,QAAQ,EAAE;4BACT,IAAI,EAAE;gCACL,MAAM,EAAE,IAAI;gCACZ,UAAU,EAAE;oCACX;wCACC,IAAI,EAAE,UAAU;wCAChB,MAAM,EAAE,IAAI;qCACZ;oCACD;wCACC,IAAI,EAAE,SAAS;wCACf,MAAM,EAAE,IAAI;qCACZ;iCACD;6BACD;yBACD;qBACD;oBACD;wBACC,EAAE,EAAE,uBAAuB;wBAC3B,WAAW,EAAE,oDAAoD;wBACjE,QAAQ,EAAE;4BACT,IAAI,EAAE;gCACL,MAAM,EAAE,SAAS;gCACjB,UAAU,EAAE;oCACX;wCACC,IAAI,EAAE,UAAU;wCAChB,MAAM,EAAE,SAAS;wCACjB,WAAW,EAAE,aAAa;qCAC1B;oCACD;wCACC,IAAI,EAAE,SAAS;wCACf,MAAM,EAAE,IAAI;qCACZ;iCACD;6BACD;yBACD;qBACD;oBACD;wBACC,EAAE,EAAE,qBAAqB;wBACzB,WAAW,EAAE,kDAAkD;wBAC/D,QAAQ,EAAE;4BACT,IAAI,EAAE;gCACL,MAAM,EAAE,OAAO;gCACf,UAAU,EAAE;oCACX;wCACC,IAAI,EAAE,UAAU;wCAChB,MAAM,EAAE,IAAI;qCACZ;oCACD;wCACC,IAAI,EAAE,SAAS;wCACf,MAAM,EAAE,OAAO;wCACf,WAAW,EAAE,aAAa;qCAC1B;iCACD;6BACD;yBACD;qBACD;iBACD;aACD;SACD;QACD,QAAQ,EAAE,IAAI;KACd,CAAC;IAEF,OAAO,CAAC,WAAW,CAAC,CAAC;AACtB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,kBAAuC,EACvC,aAAqB,EACrB,OAA0B;IAE1B,MAAM,SAAS,GAAG,gBAAgB,CAAC,GAAG,CAAmB,aAAa,CAAC,CAAC;IACxE,OAAO;QACN,IAAI,EAAE,MAAM,SAAS,CAAC,YAAY,EAAE;KACpC,CAAC;AACH,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type {\n\tIHealthComponent,\n\tIHttpRequestContext,\n\tINoContentRequest,\n\tIRestRoute,\n\tIServerHealthResponse,\n\tITag\n} from \"@twin.org/api-models\";\nimport { ComponentFactory } from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\n\n/**\n * The tag to associate with the routes.\n */\nexport const tagsHealth: ITag[] = [\n\t{\n\t\tname: \"Health\",\n\t\tdescription: \"Health endpoints for the REST server.\"\n\t}\n];\n\n/**\n * The REST routes for server health.\n * @param baseRouteName Prefix to prepend to the paths.\n * @param componentName The name of the component to use in the routes stored in the ComponentFactory.\n * @returns The generated routes.\n */\nexport function generateRestRoutesHealth(\n\tbaseRouteName: string,\n\tcomponentName: string\n): IRestRoute[] {\n\tconst healthRoute: IRestRoute<INoContentRequest, IServerHealthResponse> = {\n\t\toperationId: \"serverHealth\",\n\t\tsummary: \"Get the health for the server\",\n\t\ttag: tagsHealth[0].name,\n\t\tmethod: \"GET\",\n\t\tpath: `${baseRouteName}/`,\n\t\thandler: async (httpRequestContext, request) =>\n\t\t\tserverHealth(httpRequestContext, componentName, request),\n\t\tresponseType: [\n\t\t\t{\n\t\t\t\ttype: nameof<IServerHealthResponse>(),\n\t\t\t\texamples: [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"healthResponseOK\",\n\t\t\t\t\t\tdescription: \"The response for the health request.\",\n\t\t\t\t\t\tresponse: {\n\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\tstatus: \"ok\",\n\t\t\t\t\t\t\t\tcomponents: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tname: \"Database\",\n\t\t\t\t\t\t\t\t\t\tstatus: \"ok\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tname: \"Storage\",\n\t\t\t\t\t\t\t\t\t\tstatus: \"ok\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"healthResponseWarning\",\n\t\t\t\t\t\tdescription: \"The response for the health request with warnings.\",\n\t\t\t\t\t\tresponse: {\n\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\tstatus: \"warning\",\n\t\t\t\t\t\t\t\tcomponents: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tname: \"Database\",\n\t\t\t\t\t\t\t\t\t\tstatus: \"warning\",\n\t\t\t\t\t\t\t\t\t\tdescription: \"slowRunning\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tname: \"Storage\",\n\t\t\t\t\t\t\t\t\t\tstatus: \"ok\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tid: \"healthResponseError\",\n\t\t\t\t\t\tdescription: \"The response for the health request with errors.\",\n\t\t\t\t\t\tresponse: {\n\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\tstatus: \"error\",\n\t\t\t\t\t\t\t\tcomponents: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tname: \"Database\",\n\t\t\t\t\t\t\t\t\t\tstatus: \"ok\"\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tname: \"Storage\",\n\t\t\t\t\t\t\t\t\t\tstatus: \"error\",\n\t\t\t\t\t\t\t\t\t\tdescription: \"storageFull\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t]\n\t\t\t}\n\t\t],\n\t\tskipAuth: true\n\t};\n\n\treturn [healthRoute];\n}\n\n/**\n * Get the health for the server.\n * @param httpRequestContext The request context for the API.\n * @param componentName The name of the component to use in the routes.\n * @param request The request.\n * @returns The response object with additional http response properties.\n */\nexport async function serverHealth(\n\thttpRequestContext: IHttpRequestContext,\n\tcomponentName: string,\n\trequest: INoContentRequest\n): Promise<IServerHealthResponse> {\n\tconst component = ComponentFactory.get<IHealthComponent>(componentName);\n\treturn {\n\t\tbody: await component.healthStatus()\n\t};\n}\n"]}
@@ -0,0 +1,136 @@
1
+ import { ContextIdKeys, ContextIdStore } from "@twin.org/context";
2
+ import { BaseError, ComponentFactory, Is } from "@twin.org/core";
3
+ import { EngineCoreFactory } from "@twin.org/engine-models";
4
+ /**
5
+ * The health service for the server.
6
+ */
7
+ export class HealthService {
8
+ /**
9
+ * Runtime name for the class.
10
+ */
11
+ static CLASS_NAME = "HealthService";
12
+ /**
13
+ * The server health.
14
+ * @internal
15
+ */
16
+ _healthInfo;
17
+ /**
18
+ * The interval for checking the health of the components and setting it in the health service.
19
+ * @internal
20
+ */
21
+ _healthCheckInterval;
22
+ /**
23
+ * Interval for checking the health of the components and setting it in the health service.
24
+ * @internal
25
+ */
26
+ _healthInterval;
27
+ /**
28
+ * Create a new instance of HealthService.
29
+ * @param options The constructor options.
30
+ */
31
+ constructor(options) {
32
+ this._healthInfo = [];
33
+ this._healthCheckInterval = options?.config?.healthCheckInterval ?? 30000;
34
+ }
35
+ /**
36
+ * Returns the class name of the component.
37
+ * @returns The class name of the component.
38
+ */
39
+ className() {
40
+ return HealthService.CLASS_NAME;
41
+ }
42
+ /**
43
+ * The component needs to be started when the node is initialized.
44
+ * @param nodeLoggingComponentType The node logging component type.
45
+ * @returns Nothing.
46
+ */
47
+ async start(nodeLoggingComponentType) {
48
+ const engineCore = EngineCoreFactory.getIfExists("engine");
49
+ if (!Is.empty(engineCore)) {
50
+ this._healthInterval = globalThis.setInterval(async () => {
51
+ await ContextIdStore.run(engineCore.getContextIds() ?? {}, async () => {
52
+ const allHealth = [];
53
+ const registeredInstances = await engineCore.getRegisteredComponents();
54
+ for (const registeredInstance of registeredInstances) {
55
+ const healthMethod = registeredInstance.component.health?.bind(registeredInstance.component);
56
+ if (Is.function(healthMethod)) {
57
+ try {
58
+ allHealth.push(...(await healthMethod()));
59
+ }
60
+ catch (error) {
61
+ const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
62
+ await nodeLogging?.log({
63
+ level: "error",
64
+ source: HealthService.CLASS_NAME,
65
+ message: "componentHealthCheckFailed",
66
+ data: {
67
+ className: registeredInstance.component.className()
68
+ },
69
+ error: BaseError.fromError(error)
70
+ });
71
+ }
72
+ }
73
+ }
74
+ for (const componentHealth of allHealth) {
75
+ await this.setComponentHealth(componentHealth);
76
+ }
77
+ });
78
+ }, this._healthCheckInterval);
79
+ }
80
+ }
81
+ /**
82
+ * The component needs to be stopped when the node is closed.
83
+ * @param nodeLoggingComponentType The node logging component type.
84
+ * @returns Nothing.
85
+ */
86
+ async stop(nodeLoggingComponentType) {
87
+ if (this._healthInterval) {
88
+ clearInterval(this._healthInterval);
89
+ this._healthInterval = undefined;
90
+ }
91
+ }
92
+ /**
93
+ * Get the server health.
94
+ * @returns The service health.
95
+ */
96
+ async healthStatus() {
97
+ const contextIds = await ContextIdStore.getContextIds();
98
+ const tenantId = contextIds?.[ContextIdKeys.Tenant];
99
+ const components = this._healthInfo.filter(c => {
100
+ const componentTenantId = c.properties?.tenantId;
101
+ return Is.empty(componentTenantId) || componentTenantId === tenantId;
102
+ });
103
+ const errorCount = components.filter(c => c.status === "error").length;
104
+ const warningCount = components.filter(c => c.status === "warning").length;
105
+ let finalStatus = "ok";
106
+ if (errorCount > 0) {
107
+ finalStatus = "error";
108
+ }
109
+ else if (warningCount > 0) {
110
+ finalStatus = "warning";
111
+ }
112
+ return {
113
+ status: finalStatus,
114
+ components
115
+ };
116
+ }
117
+ /**
118
+ * Set the health status for a component.
119
+ * @param health The health of the component.
120
+ * @returns Nothing.
121
+ * @internal
122
+ */
123
+ async setComponentHealth(health) {
124
+ const componentTenantId = health.properties?.tenantId;
125
+ const componentIndex = Is.empty(componentTenantId)
126
+ ? this._healthInfo?.findIndex(c => c.name === health.name && Is.empty(c.properties?.tenantId))
127
+ : this._healthInfo?.findIndex(c => c.name === health.name && c.properties?.tenantId === componentTenantId);
128
+ if (componentIndex === -1) {
129
+ this._healthInfo.push(health);
130
+ }
131
+ else {
132
+ this._healthInfo[componentIndex] = health;
133
+ }
134
+ }
135
+ }
136
+ //# sourceMappingURL=healthService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healthService.js","sourceRoot":"","sources":["../../src/healthService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IHealthComponent } from \"@twin.org/api-models\";\nimport { ContextIdKeys, ContextIdStore } from \"@twin.org/context\";\nimport { BaseError, ComponentFactory, type HealthStatus, type IHealth, Is } from \"@twin.org/core\";\nimport { EngineCoreFactory } from \"@twin.org/engine-models\";\nimport type { ILoggingComponent } from \"@twin.org/logging-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { IHealthServiceConstructorOptions } from \"./models/IHealthServiceConstructorOptions.js\";\n\n/**\n * The health service for the server.\n */\nexport class HealthService implements IHealthComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<HealthService>();\n\n\t/**\n\t * The server health.\n\t * @internal\n\t */\n\tprivate _healthInfo: IHealth[];\n\n\t/**\n\t * The interval for checking the health of the components and setting it in the health service.\n\t * @internal\n\t */\n\tprivate readonly _healthCheckInterval: number;\n\n\t/**\n\t * Interval for checking the health of the components and setting it in the health service.\n\t * @internal\n\t */\n\tprivate _healthInterval: NodeJS.Timeout | undefined;\n\n\t/**\n\t * Create a new instance of HealthService.\n\t * @param options The constructor options.\n\t */\n\tconstructor(options?: IHealthServiceConstructorOptions) {\n\t\tthis._healthInfo = [];\n\t\tthis._healthCheckInterval = options?.config?.healthCheckInterval ?? 30000;\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 HealthService.CLASS_NAME;\n\t}\n\n\t/**\n\t * The component needs to be started when the node is initialized.\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\tconst engineCore = EngineCoreFactory.getIfExists(\"engine\");\n\n\t\tif (!Is.empty(engineCore)) {\n\t\t\tthis._healthInterval = globalThis.setInterval(async () => {\n\t\t\t\tawait ContextIdStore.run(engineCore.getContextIds() ?? {}, async () => {\n\t\t\t\t\tconst allHealth: IHealth[] = [];\n\n\t\t\t\t\tconst registeredInstances = await engineCore.getRegisteredComponents();\n\t\t\t\t\tfor (const registeredInstance of registeredInstances) {\n\t\t\t\t\t\tconst healthMethod = registeredInstance.component.health?.bind(\n\t\t\t\t\t\t\tregisteredInstance.component\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (Is.function(healthMethod)) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tallHealth.push(...(await healthMethod()));\n\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\tconst nodeLogging =\n\t\t\t\t\t\t\t\t\tComponentFactory.getIfExists<ILoggingComponent>(nodeLoggingComponentType);\n\t\t\t\t\t\t\t\tawait nodeLogging?.log({\n\t\t\t\t\t\t\t\t\tlevel: \"error\",\n\t\t\t\t\t\t\t\t\tsource: HealthService.CLASS_NAME,\n\t\t\t\t\t\t\t\t\tmessage: \"componentHealthCheckFailed\",\n\t\t\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\t\t\tclassName: registeredInstance.component.className()\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\terror: BaseError.fromError(error)\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const componentHealth of allHealth) {\n\t\t\t\t\t\tawait this.setComponentHealth(componentHealth);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}, this._healthCheckInterval);\n\t\t}\n\t}\n\n\t/**\n\t * The component needs to be stopped when the node is closed.\n\t * @param nodeLoggingComponentType The node logging component type.\n\t * @returns Nothing.\n\t */\n\tpublic async stop(nodeLoggingComponentType?: string): Promise<void> {\n\t\tif (this._healthInterval) {\n\t\t\tclearInterval(this._healthInterval);\n\t\t\tthis._healthInterval = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Get the server health.\n\t * @returns The service health.\n\t */\n\tpublic async healthStatus(): Promise<{ status: HealthStatus; components: IHealth[] }> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tconst tenantId = contextIds?.[ContextIdKeys.Tenant];\n\n\t\tconst components = this._healthInfo.filter(c => {\n\t\t\tconst componentTenantId = c.properties?.tenantId;\n\t\t\treturn Is.empty(componentTenantId) || componentTenantId === tenantId;\n\t\t});\n\n\t\tconst errorCount = components.filter(c => c.status === \"error\").length;\n\t\tconst warningCount = components.filter(c => c.status === \"warning\").length;\n\n\t\tlet finalStatus: HealthStatus = \"ok\";\n\t\tif (errorCount > 0) {\n\t\t\tfinalStatus = \"error\";\n\t\t} else if (warningCount > 0) {\n\t\t\tfinalStatus = \"warning\";\n\t\t}\n\n\t\treturn {\n\t\t\tstatus: finalStatus,\n\t\t\tcomponents\n\t\t};\n\t}\n\n\t/**\n\t * Set the health status for a component.\n\t * @param health The health of the component.\n\t * @returns Nothing.\n\t * @internal\n\t */\n\tprivate async setComponentHealth(health: IHealth): Promise<void> {\n\t\tconst componentTenantId = health.properties?.tenantId;\n\t\tconst componentIndex = Is.empty(componentTenantId)\n\t\t\t? this._healthInfo?.findIndex(c => c.name === health.name && Is.empty(c.properties?.tenantId))\n\t\t\t: this._healthInfo?.findIndex(\n\t\t\t\t\tc => c.name === health.name && c.properties?.tenantId === componentTenantId\n\t\t\t\t);\n\n\t\tif (componentIndex === -1) {\n\t\t\tthis._healthInfo.push(health);\n\t\t} else {\n\t\t\tthis._healthInfo[componentIndex] = health;\n\t\t}\n\t}\n}\n"]}
@@ -2,8 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
3
  import { HttpUrlHelper } from "@twin.org/api-models";
4
4
  import { ContextIdKeys, ContextIdStore } from "@twin.org/context";
5
- import { BaseError, ComponentFactory, Converter, GeneralError, Guards, Is, ObjectHelper, RandomHelper, Uint8ArrayHelper } from "@twin.org/core";
6
- import { VaultConnectorFactory, VaultEncryptionType } from "@twin.org/vault-models";
5
+ import { ComponentFactory, Guards, Is } from "@twin.org/core";
7
6
  /**
8
7
  * The hosting service for the server.
9
8
  */
@@ -12,33 +11,11 @@ export class HostingService {
12
11
  * Runtime name for the class.
13
12
  */
14
13
  static CLASS_NAME = "HostingService";
15
- /**
16
- * The prefix to use for encrypted query parameters when encrypting/decrypting.
17
- * This is used to identify which query parameters have been encrypted.
18
- * The actual key used in the query will be "x-enc-{originalKey}".
19
- * @internal
20
- */
21
- static _KEY_PREFIX = "x-enc-";
22
- /**
23
- * The default name for the tenant token query parameter.
24
- * @internal
25
- */
26
- static _DEFAULT_TENANT_TOKEN_NAME = "tenant-token";
27
- /**
28
- * The default name for the parameter encryption key query parameter.
29
- * @internal
30
- */
31
- static _DEFAULT_PARAM_ENCRYPTION_KEY_NAME = "param-encryption";
32
14
  /**
33
15
  * The tenant admin component.
34
16
  * @internal
35
17
  */
36
18
  _tenantAdminComponentType;
37
- /**
38
- * The vault connector type.
39
- * @internal
40
- */
41
- _vaultConnectorType;
42
19
  /**
43
20
  * The local origin URL e.g. "http://localhost:3000".
44
21
  * @internal
@@ -49,21 +26,6 @@ export class HostingService {
49
26
  * @internal
50
27
  */
51
28
  _publicOrigin;
52
- /**
53
- * The name of the key to retrieve from the vault for encryption/decryption of parameters.
54
- * @internal
55
- */
56
- _paramEncryptionKeyName;
57
- /**
58
- * The query param name carrying the encrypted tenant token.
59
- * @internal
60
- */
61
- _tenantTokenName;
62
- /**
63
- * The node identity, captured at start.
64
- * @internal
65
- */
66
- _nodeId;
67
29
  /**
68
30
  * Create a new instance of HostingService.
69
31
  * @param options The options to create the service.
@@ -73,13 +35,8 @@ export class HostingService {
73
35
  Guards.object(HostingService.CLASS_NAME, "options.config", options.config);
74
36
  Guards.stringValue(HostingService.CLASS_NAME, "options.config.localOrigin", options.config.localOrigin);
75
37
  this._tenantAdminComponentType = options?.tenantAdminComponentType ?? "tenant-admin";
76
- this._vaultConnectorType = options?.vaultConnectorType ?? "vault";
77
38
  this._localOrigin = options.config.localOrigin;
78
39
  this._publicOrigin = options.config.publicOrigin;
79
- this._paramEncryptionKeyName =
80
- options.config.paramEncryptionKeyName ?? HostingService._DEFAULT_PARAM_ENCRYPTION_KEY_NAME;
81
- this._tenantTokenName =
82
- options.config.tenantTokenName ?? HostingService._DEFAULT_TENANT_TOKEN_NAME;
83
40
  }
84
41
  /**
85
42
  * Returns the class name of the component.
@@ -88,23 +45,12 @@ export class HostingService {
88
45
  className() {
89
46
  return HostingService.CLASS_NAME;
90
47
  }
91
- /**
92
- * The component needs to be started when the node is initialized.
93
- * @param nodeLoggingComponentType The node logging component type.
94
- * @returns Nothing.
95
- */
96
- async start(nodeLoggingComponentType) {
97
- const contextIds = await ContextIdStore.getContextIds();
98
- this._nodeId = contextIds?.[ContextIdKeys.Node];
99
- }
100
48
  /**
101
49
  * Get the public origin for the hosting.
102
50
  * @param serverRequestUrl The url of the current server request if there is one.
103
51
  * @returns The public origin.
104
52
  */
105
53
  async getPublicOrigin(serverRequestUrl) {
106
- // If there is a tenant admin component, and the context has a tenant id set.
107
- // set if the tenant has a specific public origin.
108
54
  const contextIds = await ContextIdStore.getContextIds();
109
55
  const tenantId = contextIds?.[ContextIdKeys.Tenant];
110
56
  let tenantPublicOrigin;
@@ -114,10 +60,6 @@ export class HostingService {
114
60
  const serverRequestOrigin = Is.stringValue(serverRequestUrl)
115
61
  ? HttpUrlHelper.extractOrigin(serverRequestUrl)
116
62
  : undefined;
117
- // If there is a tenant public origin, return it.
118
- // If not and the config has a public origin, return it.
119
- // Otherwise, use the server request URL if provided
120
- // else fallback to local origin.
121
63
  return tenantPublicOrigin ?? this._publicOrigin ?? serverRequestOrigin ?? this._localOrigin;
122
64
  }
123
65
  /**
@@ -143,155 +85,5 @@ export class HostingService {
143
85
  const publicOrigin = await this.getPublicOrigin(url);
144
86
  return HttpUrlHelper.replaceOrigin(url, publicOrigin);
145
87
  }
146
- /**
147
- * Add encrypted key/value pairs to a URL's query string.
148
- * Existing query parameters on the URL are preserved; the provided params are
149
- * merged in and then encrypted before being written back to the URL.
150
- * @param url The base URL to add parameters to.
151
- * @param params The key/value pairs to encrypt and append.
152
- * @returns The URL with the encrypted parameters added.
153
- */
154
- async addEncryptedParamsToUrl(url, params) {
155
- const urlObj = new URL(url);
156
- const query = {};
157
- for (const [key, value] of urlObj.searchParams.entries()) {
158
- query[key] = value;
159
- }
160
- const keysToEncrypt = Object.keys(params);
161
- for (const [key, value] of Object.entries(params)) {
162
- query[key] = value;
163
- }
164
- await this.encryptQueryParams(query, keysToEncrypt);
165
- urlObj.search = "";
166
- for (const [key, value] of Object.entries(query)) {
167
- urlObj.searchParams.set(key, value);
168
- }
169
- return urlObj.toString();
170
- }
171
- /**
172
- * Decrypt specified keys from a query parameter object and return their plain-text values.
173
- * @param queryParams The HTTP request query containing the encrypted parameters.
174
- * @param keys The keys to decrypt.
175
- * @returns A map of the decrypted key/value pairs that were present.
176
- */
177
- async getDecryptedParamsFromQueryParams(queryParams, keys) {
178
- const queryParamsClone = ObjectHelper.clone(queryParams) ?? {};
179
- await this.decryptQueryParams(queryParamsClone, keys);
180
- const result = {};
181
- for (const key of keys) {
182
- if (Is.stringValue(queryParamsClone[key])) {
183
- result[key] = queryParamsClone[key];
184
- }
185
- }
186
- return result;
187
- }
188
- /**
189
- * Encrypt the tenant id and append it as a query parameter to the given URL.
190
- * @param url The URL to append the encrypted tenant token to.
191
- * @param tenantId The tenant identifier to encrypt and add.
192
- * @returns The URL with the encrypted tenant token added as a query parameter.
193
- */
194
- async addTenantTokenToUrl(url, tenantId) {
195
- return this.addEncryptedParamsToUrl(url, { [this._tenantTokenName]: tenantId });
196
- }
197
- /**
198
- * Get the tenant token from the query parameters.
199
- * @param queryParams The HTTP request query containing the parameters.
200
- * @returns The tenant token if it exists.
201
- */
202
- async getTenantTokenFromQueryParams(queryParams) {
203
- const decrypted = await this.getDecryptedParamsFromQueryParams(queryParams, [
204
- this._tenantTokenName
205
- ]);
206
- return decrypted[this._tenantTokenName];
207
- }
208
- /**
209
- * Encrypt query parameters using the hosting component's encryption mechanism.
210
- * @param httpRequestQuery The HTTP request query containing the parameters to encrypt.
211
- * @param keys The keys of the parameters to encrypt.
212
- * @returns A promise that resolves when the query parameters have been encrypted.
213
- */
214
- async encryptQueryParams(httpRequestQuery, keys) {
215
- if (Is.empty(httpRequestQuery)) {
216
- return;
217
- }
218
- for (const key of keys) {
219
- if (Is.stringValue(httpRequestQuery[key])) {
220
- const encryptedValue = await this.encryptParam(httpRequestQuery[key]);
221
- httpRequestQuery[`${HostingService._KEY_PREFIX}${key}`] = encryptedValue;
222
- delete httpRequestQuery[key];
223
- }
224
- }
225
- }
226
- /**
227
- * Decrypt query parameters using the hosting component's encryption mechanism.
228
- * @param httpRequestQuery The HTTP request query containing the encrypted values.
229
- * @param keys The keys of the parameters to decrypt.
230
- * @returns A promise that resolves when the query parameters have been decrypted.
231
- */
232
- async decryptQueryParams(httpRequestQuery, keys) {
233
- if (Is.empty(httpRequestQuery)) {
234
- return;
235
- }
236
- for (const key of Object.keys(httpRequestQuery)) {
237
- if (key.startsWith(HostingService._KEY_PREFIX)) {
238
- const originalKey = key.slice(HostingService._KEY_PREFIX.length);
239
- if (keys.includes(originalKey)) {
240
- const decryptedValue = await this.decryptParam(httpRequestQuery[key]);
241
- httpRequestQuery[originalKey] = decryptedValue;
242
- delete httpRequestQuery[key];
243
- }
244
- }
245
- }
246
- }
247
- /**
248
- * Encrypt a parameter value using the hosting component's encryption mechanism.
249
- * @param paramValue The value of the parameter to encrypt.
250
- * @returns A promise that resolves to the encrypted value of the parameter.
251
- */
252
- async encryptParam(paramValue) {
253
- Guards.stringValue(HostingService.CLASS_NAME, "paramValue", paramValue);
254
- const vaultConnector = VaultConnectorFactory.getIfExists(this._vaultConnectorType);
255
- if (Is.empty(vaultConnector) || Is.empty(this._nodeId)) {
256
- throw new GeneralError(HostingService.CLASS_NAME, "encryptionUnavailable");
257
- }
258
- try {
259
- // Add a salt to the encryption to ensure that the same value encrypted multiple times will yield different results,
260
- // preventing rainbow table attacks.
261
- const salt = RandomHelper.generate(8);
262
- const encryptedParamValue = await vaultConnector.encrypt(`${this._nodeId}/${this._paramEncryptionKeyName}`, VaultEncryptionType.ChaCha20Poly1305, Uint8ArrayHelper.concat([salt, Converter.utf8ToBytes(paramValue)]));
263
- if (!Is.uint8Array(encryptedParamValue)) {
264
- throw new GeneralError(HostingService.CLASS_NAME, "encryptionFailed");
265
- }
266
- return Converter.bytesToBase64Url(encryptedParamValue);
267
- }
268
- catch (err) {
269
- throw new GeneralError(HostingService.CLASS_NAME, "encryptionFailed", undefined, BaseError.fromError(err));
270
- }
271
- }
272
- /**
273
- * Decrypt a parameter value using the hosting component's encryption mechanism.
274
- * @param encryptedValue The encrypted value of the parameter.
275
- * @returns A promise that resolves to the decrypted value of the parameter.
276
- */
277
- async decryptParam(encryptedValue) {
278
- Guards.stringValue(HostingService.CLASS_NAME, "encryptedValue", encryptedValue);
279
- const vaultConnector = VaultConnectorFactory.getIfExists(this._vaultConnectorType);
280
- if (Is.empty(vaultConnector) || Is.empty(this._nodeId)) {
281
- throw new GeneralError(HostingService.CLASS_NAME, "decryptionUnavailable");
282
- }
283
- try {
284
- const encryptedBytes = Converter.base64UrlToBytes(encryptedValue);
285
- const decryptedBytes = await vaultConnector.decrypt(`${this._nodeId}/${this._paramEncryptionKeyName}`, VaultEncryptionType.ChaCha20Poly1305, encryptedBytes);
286
- if (!Is.uint8Array(decryptedBytes)) {
287
- throw new GeneralError(HostingService.CLASS_NAME, "decryptionFailed");
288
- }
289
- // The decrypted value is expected to have the salt as the first 8 bytes, which we need to remove before returning the original value.
290
- return Converter.bytesToUtf8(decryptedBytes.slice(8));
291
- }
292
- catch (err) {
293
- throw new GeneralError(HostingService.CLASS_NAME, "decryptionFailed", undefined, BaseError.fromError(err));
294
- }
295
- }
296
88
  }
297
89
  //# sourceMappingURL=hostingService.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hostingService.js","sourceRoot":"","sources":["../../src/hostingService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tHttpUrlHelper,\n\ttype IHostingComponent,\n\ttype IHttpRequestQuery,\n\ttype ITenantAdminComponent\n} from \"@twin.org/api-models\";\nimport { ContextIdKeys, ContextIdStore } from \"@twin.org/context\";\nimport {\n\tBaseError,\n\tComponentFactory,\n\tConverter,\n\tGeneralError,\n\tGuards,\n\tIs,\n\tObjectHelper,\n\tRandomHelper,\n\tUint8ArrayHelper\n} from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { VaultConnectorFactory, VaultEncryptionType } from \"@twin.org/vault-models\";\nimport type { IHostingServiceConstructorOptions } from \"./models/IHostingServiceConstructorOptions.js\";\n\n/**\n * The hosting service for the server.\n */\nexport class HostingService implements IHostingComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<HostingService>();\n\n\t/**\n\t * The prefix to use for encrypted query parameters when encrypting/decrypting.\n\t * This is used to identify which query parameters have been encrypted.\n\t * The actual key used in the query will be \"x-enc-{originalKey}\".\n\t * @internal\n\t */\n\tprivate static readonly _KEY_PREFIX = \"x-enc-\";\n\n\t/**\n\t * The default name for the tenant token query parameter.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_TENANT_TOKEN_NAME: string = \"tenant-token\";\n\n\t/**\n\t * The default name for the parameter encryption key query parameter.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_PARAM_ENCRYPTION_KEY_NAME: string = \"param-encryption\";\n\n\t/**\n\t * The tenant admin component.\n\t * @internal\n\t */\n\tprivate readonly _tenantAdminComponentType?: string;\n\n\t/**\n\t * The vault connector type.\n\t * @internal\n\t */\n\tprivate readonly _vaultConnectorType?: string;\n\n\t/**\n\t * The local origin URL e.g. \"http://localhost:3000\".\n\t * @internal\n\t */\n\tprivate readonly _localOrigin: string;\n\n\t/**\n\t * The APIs public base URL e.g. \"https://api.example.com:1234\".\n\t * @internal\n\t */\n\tprivate readonly _publicOrigin?: string;\n\n\t/**\n\t * The name of the key to retrieve from the vault for encryption/decryption of parameters.\n\t * @internal\n\t */\n\tprivate readonly _paramEncryptionKeyName: 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 node identity, captured at start.\n\t * @internal\n\t */\n\tprivate _nodeId?: string;\n\n\t/**\n\t * Create a new instance of HostingService.\n\t * @param options The options to create the service.\n\t */\n\tconstructor(options: IHostingServiceConstructorOptions) {\n\t\tGuards.object(HostingService.CLASS_NAME, nameof(options), options);\n\t\tGuards.object(HostingService.CLASS_NAME, nameof(options.config), options.config);\n\t\tGuards.stringValue(\n\t\t\tHostingService.CLASS_NAME,\n\t\t\tnameof(options.config.localOrigin),\n\t\t\toptions.config.localOrigin\n\t\t);\n\t\tthis._tenantAdminComponentType = options?.tenantAdminComponentType ?? \"tenant-admin\";\n\t\tthis._vaultConnectorType = options?.vaultConnectorType ?? \"vault\";\n\t\tthis._localOrigin = options.config.localOrigin;\n\t\tthis._publicOrigin = options.config.publicOrigin;\n\t\tthis._paramEncryptionKeyName =\n\t\t\toptions.config.paramEncryptionKeyName ?? HostingService._DEFAULT_PARAM_ENCRYPTION_KEY_NAME;\n\t\tthis._tenantTokenName =\n\t\t\toptions.config.tenantTokenName ?? HostingService._DEFAULT_TENANT_TOKEN_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 HostingService.CLASS_NAME;\n\t}\n\n\t/**\n\t * The component needs to be started when the node is initialized.\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\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tthis._nodeId = contextIds?.[ContextIdKeys.Node];\n\t}\n\n\t/**\n\t * Get the public origin for the hosting.\n\t * @param serverRequestUrl The url of the current server request if there is one.\n\t * @returns The public origin.\n\t */\n\tpublic async getPublicOrigin(serverRequestUrl?: string): Promise<string> {\n\t\t// If there is a tenant admin component, and the context has a tenant id set.\n\t\t// set if the tenant has a specific public origin.\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tconst tenantId = contextIds?.[ContextIdKeys.Tenant];\n\t\tlet tenantPublicOrigin;\n\t\tif (Is.stringValue(tenantId)) {\n\t\t\ttenantPublicOrigin = await this.getTenantOrigin(tenantId);\n\t\t}\n\n\t\tconst serverRequestOrigin = Is.stringValue(serverRequestUrl)\n\t\t\t? HttpUrlHelper.extractOrigin(serverRequestUrl)\n\t\t\t: undefined;\n\n\t\t// If there is a tenant public origin, return it.\n\t\t// If not and the config has a public origin, return it.\n\t\t// Otherwise, use the server request URL if provided\n\t\t// else fallback to local origin.\n\t\treturn tenantPublicOrigin ?? this._publicOrigin ?? serverRequestOrigin ?? this._localOrigin;\n\t}\n\n\t/**\n\t * Get the public origin for the tenant if one exists.\n\t * @param tenantId The tenant identifier.\n\t * @returns The public origin for the tenant.\n\t */\n\tpublic async getTenantOrigin(tenantId: string): Promise<string | undefined> {\n\t\tGuards.stringHexLength(HostingService.CLASS_NAME, nameof(tenantId), tenantId, 32);\n\n\t\tconst tenantAdminComponent = ComponentFactory.getIfExists<ITenantAdminComponent>(\n\t\t\tthis._tenantAdminComponentType\n\t\t);\n\n\t\tif (Is.empty(tenantAdminComponent)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst tenant = await tenantAdminComponent.get(tenantId);\n\t\treturn tenant?.publicOrigin;\n\t}\n\n\t/**\n\t * Build a public url based on the public origin and the url provided.\n\t * @param url The url to build upon the public origin.\n\t * @returns The full url based on the public origin.\n\t */\n\tpublic async buildPublicUrl(url: string): Promise<string> {\n\t\tconst publicOrigin = await this.getPublicOrigin(url);\n\t\treturn HttpUrlHelper.replaceOrigin(url, publicOrigin);\n\t}\n\n\t/**\n\t * Add encrypted key/value pairs to a URL's query string.\n\t * Existing query parameters on the URL are preserved; the provided params are\n\t * merged in and then encrypted before being written back to the URL.\n\t * @param url The base URL to add parameters to.\n\t * @param params The key/value pairs to encrypt and append.\n\t * @returns The URL with the encrypted parameters added.\n\t */\n\tpublic async addEncryptedParamsToUrl(url: string, params: IHttpRequestQuery): Promise<string> {\n\t\tconst urlObj = new URL(url);\n\t\tconst query: IHttpRequestQuery = {};\n\t\tfor (const [key, value] of urlObj.searchParams.entries()) {\n\t\t\tquery[key] = value;\n\t\t}\n\t\tconst keysToEncrypt = Object.keys(params);\n\t\tfor (const [key, value] of Object.entries(params)) {\n\t\t\tquery[key] = value;\n\t\t}\n\t\tawait this.encryptQueryParams(query, keysToEncrypt);\n\t\turlObj.search = \"\";\n\t\tfor (const [key, value] of Object.entries(query)) {\n\t\t\turlObj.searchParams.set(key, value);\n\t\t}\n\t\treturn urlObj.toString();\n\t}\n\n\t/**\n\t * Decrypt specified keys from a query parameter object and return their plain-text values.\n\t * @param queryParams The HTTP request query containing the encrypted parameters.\n\t * @param keys The keys to decrypt.\n\t * @returns A map of the decrypted key/value pairs that were present.\n\t */\n\tpublic async getDecryptedParamsFromQueryParams(\n\t\tqueryParams: IHttpRequestQuery | undefined,\n\t\tkeys: string[]\n\t): Promise<IHttpRequestQuery> {\n\t\tconst queryParamsClone = ObjectHelper.clone(queryParams) ?? {};\n\t\tawait this.decryptQueryParams(queryParamsClone, keys);\n\t\tconst result: IHttpRequestQuery = {};\n\t\tfor (const key of keys) {\n\t\t\tif (Is.stringValue(queryParamsClone[key])) {\n\t\t\t\tresult[key] = queryParamsClone[key];\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Encrypt the tenant id and append it as a query parameter to the given URL.\n\t * @param url The URL to append the encrypted tenant token to.\n\t * @param tenantId The tenant identifier to encrypt and add.\n\t * @returns The URL with the encrypted tenant token added as a query parameter.\n\t */\n\tpublic async addTenantTokenToUrl(url: string, tenantId: string): Promise<string> {\n\t\treturn this.addEncryptedParamsToUrl(url, { [this._tenantTokenName]: tenantId });\n\t}\n\n\t/**\n\t * Get the tenant token from the query parameters.\n\t * @param queryParams The HTTP request query containing the parameters.\n\t * @returns The tenant token if it exists.\n\t */\n\tpublic async getTenantTokenFromQueryParams(\n\t\tqueryParams: IHttpRequestQuery | undefined\n\t): Promise<string | undefined> {\n\t\tconst decrypted = await this.getDecryptedParamsFromQueryParams(queryParams, [\n\t\t\tthis._tenantTokenName\n\t\t]);\n\t\treturn decrypted[this._tenantTokenName];\n\t}\n\n\t/**\n\t * Encrypt query parameters using the hosting component's encryption mechanism.\n\t * @param httpRequestQuery The HTTP request query containing the parameters to encrypt.\n\t * @param keys The keys of the parameters to encrypt.\n\t * @returns A promise that resolves when the query parameters have been encrypted.\n\t */\n\tpublic async encryptQueryParams(\n\t\thttpRequestQuery: IHttpRequestQuery | undefined,\n\t\tkeys: string[]\n\t): Promise<void> {\n\t\tif (Is.empty(httpRequestQuery)) {\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const key of keys) {\n\t\t\tif (Is.stringValue(httpRequestQuery[key])) {\n\t\t\t\tconst encryptedValue = await this.encryptParam(httpRequestQuery[key]);\n\t\t\t\thttpRequestQuery[`${HostingService._KEY_PREFIX}${key}`] = encryptedValue;\n\t\t\t\tdelete httpRequestQuery[key];\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Decrypt query parameters using the hosting component's encryption mechanism.\n\t * @param httpRequestQuery The HTTP request query containing the encrypted values.\n\t * @param keys The keys of the parameters to decrypt.\n\t * @returns A promise that resolves when the query parameters have been decrypted.\n\t */\n\tpublic async decryptQueryParams(\n\t\thttpRequestQuery: IHttpRequestQuery | undefined,\n\t\tkeys: string[]\n\t): Promise<void> {\n\t\tif (Is.empty(httpRequestQuery)) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const key of Object.keys(httpRequestQuery)) {\n\t\t\tif (key.startsWith(HostingService._KEY_PREFIX)) {\n\t\t\t\tconst originalKey = key.slice(HostingService._KEY_PREFIX.length);\n\n\t\t\t\tif (keys.includes(originalKey)) {\n\t\t\t\t\tconst decryptedValue = await this.decryptParam(httpRequestQuery[key]);\n\t\t\t\t\thttpRequestQuery[originalKey] = decryptedValue;\n\t\t\t\t\tdelete httpRequestQuery[key];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Encrypt a parameter value using the hosting component's encryption mechanism.\n\t * @param paramValue The value of the parameter to encrypt.\n\t * @returns A promise that resolves to the encrypted value of the parameter.\n\t */\n\tpublic async encryptParam(paramValue: string): Promise<string> {\n\t\tGuards.stringValue(HostingService.CLASS_NAME, nameof(paramValue), paramValue);\n\n\t\tconst vaultConnector = VaultConnectorFactory.getIfExists(this._vaultConnectorType);\n\t\tif (Is.empty(vaultConnector) || Is.empty(this._nodeId)) {\n\t\t\tthrow new GeneralError(HostingService.CLASS_NAME, \"encryptionUnavailable\");\n\t\t}\n\n\t\ttry {\n\t\t\t// Add a salt to the encryption to ensure that the same value encrypted multiple times will yield different results,\n\t\t\t// preventing rainbow table attacks.\n\t\t\tconst salt = RandomHelper.generate(8);\n\n\t\t\tconst encryptedParamValue = await vaultConnector.encrypt(\n\t\t\t\t`${this._nodeId}/${this._paramEncryptionKeyName}`,\n\t\t\t\tVaultEncryptionType.ChaCha20Poly1305,\n\t\t\t\tUint8ArrayHelper.concat([salt, Converter.utf8ToBytes(paramValue)])\n\t\t\t);\n\n\t\t\tif (!Is.uint8Array(encryptedParamValue)) {\n\t\t\t\tthrow new GeneralError(HostingService.CLASS_NAME, \"encryptionFailed\");\n\t\t\t}\n\n\t\t\treturn Converter.bytesToBase64Url(encryptedParamValue);\n\t\t} catch (err) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tHostingService.CLASS_NAME,\n\t\t\t\t\"encryptionFailed\",\n\t\t\t\tundefined,\n\t\t\t\tBaseError.fromError(err)\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Decrypt a parameter value using the hosting component's encryption mechanism.\n\t * @param encryptedValue The encrypted value of the parameter.\n\t * @returns A promise that resolves to the decrypted value of the parameter.\n\t */\n\tpublic async decryptParam(encryptedValue: string): Promise<string> {\n\t\tGuards.stringValue(HostingService.CLASS_NAME, nameof(encryptedValue), encryptedValue);\n\n\t\tconst vaultConnector = VaultConnectorFactory.getIfExists(this._vaultConnectorType);\n\t\tif (Is.empty(vaultConnector) || Is.empty(this._nodeId)) {\n\t\t\tthrow new GeneralError(HostingService.CLASS_NAME, \"decryptionUnavailable\");\n\t\t}\n\n\t\ttry {\n\t\t\tconst encryptedBytes = Converter.base64UrlToBytes(encryptedValue);\n\t\t\tconst decryptedBytes = await vaultConnector.decrypt(\n\t\t\t\t`${this._nodeId}/${this._paramEncryptionKeyName}`,\n\t\t\t\tVaultEncryptionType.ChaCha20Poly1305,\n\t\t\t\tencryptedBytes\n\t\t\t);\n\n\t\t\tif (!Is.uint8Array(decryptedBytes)) {\n\t\t\t\tthrow new GeneralError(HostingService.CLASS_NAME, \"decryptionFailed\");\n\t\t\t}\n\n\t\t\t// The decrypted value is expected to have the salt as the first 8 bytes, which we need to remove before returning the original value.\n\t\t\treturn Converter.bytesToUtf8(decryptedBytes.slice(8));\n\t\t} catch (err) {\n\t\t\tthrow new GeneralError(\n\t\t\t\tHostingService.CLASS_NAME,\n\t\t\t\t\"decryptionFailed\",\n\t\t\t\tundefined,\n\t\t\t\tBaseError.fromError(err)\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"hostingService.js","sourceRoot":"","sources":["../../src/hostingService.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EACN,aAAa,EAGb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAI9D;;GAEG;AACH,MAAM,OAAO,cAAc;IAC1B;;OAEG;IACI,MAAM,CAAU,UAAU,oBAAoC;IAErE;;;OAGG;IACc,yBAAyB,CAAU;IAEpD;;;OAGG;IACc,YAAY,CAAS;IAEtC;;;OAGG;IACc,aAAa,CAAU;IAExC;;;OAGG;IACH,YAAY,OAA0C;QACrD,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,UAAU,aAAmB,OAAO,CAAC,CAAC;QACnE,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,UAAU,oBAA0B,OAAO,CAAC,MAAM,CAAC,CAAC;QACjF,MAAM,CAAC,WAAW,CACjB,cAAc,CAAC,UAAU,gCAEzB,OAAO,CAAC,MAAM,CAAC,WAAW,CAC1B,CAAC;QACF,IAAI,CAAC,yBAAyB,GAAG,OAAO,EAAE,wBAAwB,IAAI,cAAc,CAAC;QACrF,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC;QAC/C,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC;IAClD,CAAC;IAED;;;OAGG;IACI,SAAS;QACf,OAAO,cAAc,CAAC,UAAU,CAAC;IAClC,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,eAAe,CAAC,gBAAyB;QACrD,MAAM,UAAU,GAAG,MAAM,cAAc,CAAC,aAAa,EAAE,CAAC;QACxD,MAAM,QAAQ,GAAG,UAAU,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACpD,IAAI,kBAAkB,CAAC;QACvB,IAAI,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9B,kBAAkB,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QAC3D,CAAC;QAED,MAAM,mBAAmB,GAAG,EAAE,CAAC,WAAW,CAAC,gBAAgB,CAAC;YAC3D,CAAC,CAAC,aAAa,CAAC,aAAa,CAAC,gBAAgB,CAAC;YAC/C,CAAC,CAAC,SAAS,CAAC;QAEb,OAAO,kBAAkB,IAAI,IAAI,CAAC,aAAa,IAAI,mBAAmB,IAAI,IAAI,CAAC,YAAY,CAAC;IAC7F,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,eAAe,CAAC,QAAgB;QAC5C,MAAM,CAAC,eAAe,CAAC,cAAc,CAAC,UAAU,cAAoB,QAAQ,EAAE,EAAE,CAAC,CAAC;QAElF,MAAM,oBAAoB,GAAG,gBAAgB,CAAC,WAAW,CACxD,IAAI,CAAC,yBAAyB,CAC9B,CAAC;QAEF,IAAI,EAAE,CAAC,KAAK,CAAC,oBAAoB,CAAC,EAAE,CAAC;YACpC,OAAO,SAAS,CAAC;QAClB,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,oBAAoB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACxD,OAAO,MAAM,EAAE,YAAY,CAAC;IAC7B,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,cAAc,CAAC,GAAW;QACtC,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QACrD,OAAO,aAAa,CAAC,aAAa,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACvD,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tHttpUrlHelper,\n\ttype IHostingComponent,\n\ttype ITenantAdminComponent\n} from \"@twin.org/api-models\";\nimport { ContextIdKeys, ContextIdStore } from \"@twin.org/context\";\nimport { ComponentFactory, Guards, Is } from \"@twin.org/core\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { IHostingServiceConstructorOptions } from \"./models/IHostingServiceConstructorOptions.js\";\n\n/**\n * The hosting service for the server.\n */\nexport class HostingService implements IHostingComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<HostingService>();\n\n\t/**\n\t * The tenant admin component.\n\t * @internal\n\t */\n\tprivate readonly _tenantAdminComponentType?: string;\n\n\t/**\n\t * The local origin URL e.g. \"http://localhost:3000\".\n\t * @internal\n\t */\n\tprivate readonly _localOrigin: string;\n\n\t/**\n\t * The APIs public base URL e.g. \"https://api.example.com:1234\".\n\t * @internal\n\t */\n\tprivate readonly _publicOrigin?: string;\n\n\t/**\n\t * Create a new instance of HostingService.\n\t * @param options The options to create the service.\n\t */\n\tconstructor(options: IHostingServiceConstructorOptions) {\n\t\tGuards.object(HostingService.CLASS_NAME, nameof(options), options);\n\t\tGuards.object(HostingService.CLASS_NAME, nameof(options.config), options.config);\n\t\tGuards.stringValue(\n\t\t\tHostingService.CLASS_NAME,\n\t\t\tnameof(options.config.localOrigin),\n\t\t\toptions.config.localOrigin\n\t\t);\n\t\tthis._tenantAdminComponentType = options?.tenantAdminComponentType ?? \"tenant-admin\";\n\t\tthis._localOrigin = options.config.localOrigin;\n\t\tthis._publicOrigin = options.config.publicOrigin;\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 HostingService.CLASS_NAME;\n\t}\n\n\t/**\n\t * Get the public origin for the hosting.\n\t * @param serverRequestUrl The url of the current server request if there is one.\n\t * @returns The public origin.\n\t */\n\tpublic async getPublicOrigin(serverRequestUrl?: string): Promise<string> {\n\t\tconst contextIds = await ContextIdStore.getContextIds();\n\t\tconst tenantId = contextIds?.[ContextIdKeys.Tenant];\n\t\tlet tenantPublicOrigin;\n\t\tif (Is.stringValue(tenantId)) {\n\t\t\ttenantPublicOrigin = await this.getTenantOrigin(tenantId);\n\t\t}\n\n\t\tconst serverRequestOrigin = Is.stringValue(serverRequestUrl)\n\t\t\t? HttpUrlHelper.extractOrigin(serverRequestUrl)\n\t\t\t: undefined;\n\n\t\treturn tenantPublicOrigin ?? this._publicOrigin ?? serverRequestOrigin ?? this._localOrigin;\n\t}\n\n\t/**\n\t * Get the public origin for the tenant if one exists.\n\t * @param tenantId The tenant identifier.\n\t * @returns The public origin for the tenant.\n\t */\n\tpublic async getTenantOrigin(tenantId: string): Promise<string | undefined> {\n\t\tGuards.stringHexLength(HostingService.CLASS_NAME, nameof(tenantId), tenantId, 32);\n\n\t\tconst tenantAdminComponent = ComponentFactory.getIfExists<ITenantAdminComponent>(\n\t\t\tthis._tenantAdminComponentType\n\t\t);\n\n\t\tif (Is.empty(tenantAdminComponent)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst tenant = await tenantAdminComponent.get(tenantId);\n\t\treturn tenant?.publicOrigin;\n\t}\n\n\t/**\n\t * Build a public url based on the public origin and the url provided.\n\t * @param url The url to build upon the public origin.\n\t * @returns The full url based on the public origin.\n\t */\n\tpublic async buildPublicUrl(url: string): Promise<string> {\n\t\tconst publicOrigin = await this.getPublicOrigin(url);\n\t\treturn HttpUrlHelper.replaceOrigin(url, publicOrigin);\n\t}\n}\n"]}
package/dist/es/index.js CHANGED
@@ -1,11 +1,18 @@
1
1
  // Copyright 2024 IOTA Stiftung.
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
+ export * from "./healthRoutes.js";
4
+ export * from "./healthService.js";
3
5
  export * from "./hostingService.js";
4
6
  export * from "./informationRoutes.js";
5
7
  export * from "./informationService.js";
8
+ export * from "./urlTransformerService.js";
9
+ export * from "./models/IHealthServiceConfig.js";
10
+ export * from "./models/IHealthServiceConstructorOptions.js";
6
11
  export * from "./models/IHostingServiceConfig.js";
7
12
  export * from "./models/IHostingServiceConstructorOptions.js";
8
13
  export * from "./models/IInformationServiceConfig.js";
9
14
  export * from "./models/IInformationServiceConstructorOptions.js";
15
+ export * from "./models/IUrlTransformerServiceConfig.js";
16
+ export * from "./models/IUrlTransformerServiceConstructorOptions.js";
10
17
  export * from "./restEntryPoints.js";
11
18
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,mCAAmC,CAAC;AAClD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,uCAAuC,CAAC;AACtD,cAAc,mDAAmD,CAAC;AAClE,cAAc,sBAAsB,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./hostingService.js\";\nexport * from \"./informationRoutes.js\";\nexport * from \"./informationService.js\";\nexport * from \"./models/IHostingServiceConfig.js\";\nexport * from \"./models/IHostingServiceConstructorOptions.js\";\nexport * from \"./models/IInformationServiceConfig.js\";\nexport * from \"./models/IInformationServiceConstructorOptions.js\";\nexport * from \"./restEntryPoints.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kCAAkC,CAAC;AACjD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,mCAAmC,CAAC;AAClD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,uCAAuC,CAAC;AACtD,cAAc,mDAAmD,CAAC;AAClE,cAAc,0CAA0C,CAAC;AACzD,cAAc,sDAAsD,CAAC;AACrE,cAAc,sBAAsB,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./healthRoutes.js\";\nexport * from \"./healthService.js\";\nexport * from \"./hostingService.js\";\nexport * from \"./informationRoutes.js\";\nexport * from \"./informationService.js\";\nexport * from \"./urlTransformerService.js\";\nexport * from \"./models/IHealthServiceConfig.js\";\nexport * from \"./models/IHealthServiceConstructorOptions.js\";\nexport * from \"./models/IHostingServiceConfig.js\";\nexport * from \"./models/IHostingServiceConstructorOptions.js\";\nexport * from \"./models/IInformationServiceConfig.js\";\nexport * from \"./models/IInformationServiceConstructorOptions.js\";\nexport * from \"./models/IUrlTransformerServiceConfig.js\";\nexport * from \"./models/IUrlTransformerServiceConstructorOptions.js\";\nexport * from \"./restEntryPoints.js\";\n"]}