@apolitical/server 4.2.0 → 4.3.0-pla-471.0

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/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@apolitical/server",
3
- "version": "4.2.0",
3
+ "version": "4.3.0-pla-471.0",
4
4
  "description": "Node.js module to encapsulate Apolitical's express server setup",
5
5
  "author": "Apolitical Group Limited <engineering@apolitical.co>",
6
6
  "license": "MIT",
7
7
  "main": "src/index.js",
8
+ "exports": {
9
+ ".": "./src/index.js",
10
+ "./secrets": "./src/services/secret-manager.service.js"
11
+ },
8
12
  "files": [
9
13
  "src"
10
14
  ],
@@ -23,7 +27,7 @@
23
27
  "Node Modules"
24
28
  ],
25
29
  "dependencies": {
26
- "@apolitical/logger": "2.1.2",
30
+ "@apolitical/logger": "3.0.0",
27
31
  "@cloudnative/health-connect": "2.1.0",
28
32
  "@google-cloud/secret-manager": "6.1.1",
29
33
  "@opentelemetry/api": "1.9.0",
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');
4
+
5
+ /**
6
+ * Thin wrapper around Google Cloud Secret Manager.
7
+ *
8
+ * Every method is static because the class is consumed before the Awilix DI
9
+ * container is available (e.g. inside app.js at startup).
10
+ * The underlying client is lazily created and reused across all calls.
11
+ */
12
+ class SecretManagerService {
13
+ /**
14
+ * Derive the secret name prefix from the PLATFORM_BASE_URL subdomain.
15
+ * - https://apolitical.co -> "live"
16
+ * - https://[beta|rc].apolitical.co -> "[beta|rc]"
17
+ * - https://[beta|rc]-[e1|a1].apolitical.co -> "[beta|rc]"
18
+ * - https://localhost -> "tilt" (Local Tilt)
19
+ * - http://localhost -> "pnpm" (Local standalone)
20
+ */
21
+ static getSecretPrefix(useLocal) {
22
+ const baseURL = String(process.env.PLATFORM_BASE_URL ?? '');
23
+ try {
24
+ const hostname = new URL(baseURL).hostname;
25
+ if (hostname === 'apolitical.co') return 'live';
26
+ if (hostname === 'localhost' && useLocal)
27
+ return baseURL.startsWith('https') ? 'tilt' : 'pnpm';
28
+ const parts = hostname.split('.');
29
+ if (parts.length > 2 && hostname.endsWith('.apolitical.co'))
30
+ return parts[0].replace(/-[a-z]\d+$/, '');
31
+ } catch {
32
+ /* fall through */
33
+ }
34
+ return useLocal ? 'pnpm' : 'beta';
35
+ }
36
+
37
+ static async _getClient() {
38
+ if (!this._clientPromise) {
39
+ this._clientPromise = (async () => {
40
+ // eslint-disable-next-line no-console
41
+ console.log('[SecretManager] Initialising client.');
42
+ return new SecretManagerServiceClient();
43
+ })();
44
+ }
45
+ return this._clientPromise;
46
+ }
47
+
48
+ /**
49
+ * Access a secret's payload from Google Secret Manager.
50
+ *
51
+ * The full secret name is built as `{prefix}_{secretBaseName}`, where the
52
+ * prefix is derived from PLATFORM_BASE_URL (e.g. "beta", "live").
53
+ */
54
+ static async accessSecret(secretBaseName, options) {
55
+ const client = await this._getClient();
56
+ const projectId = process.env.GCP_PROJECT_ID ?? (await client.auth.getProjectId());
57
+
58
+ const prefix = this.getSecretPrefix(options?.useLocal);
59
+ const secretName = `${prefix}_${secretBaseName}`;
60
+ const version = options?.version ?? 'latest';
61
+
62
+ const name = `projects/${projectId}/secrets/${secretName}/versions/${version}`;
63
+ const [response] = await client.accessSecretVersion({ name });
64
+
65
+ const payload = response.payload?.data;
66
+ if (!payload) {
67
+ throw new Error(`Empty payload for secret "${secretName}"`);
68
+ }
69
+
70
+ return typeof payload === 'string' ? payload : payload.toString('utf8');
71
+ }
72
+ }
73
+
74
+ module.exports = { SecretManagerService };