@platform-x/shp-auth-client 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +304 -0
  2. package/dist/cache.d.ts +10 -0
  3. package/dist/cache.d.ts.map +1 -0
  4. package/dist/cache.js +28 -0
  5. package/dist/cache.js.map +1 -0
  6. package/dist/config-provider.d.ts +17 -0
  7. package/dist/config-provider.d.ts.map +1 -0
  8. package/dist/config-provider.js +54 -0
  9. package/dist/config-provider.js.map +1 -0
  10. package/dist/errors.d.ts +53 -0
  11. package/dist/errors.d.ts.map +1 -0
  12. package/dist/errors.js +117 -0
  13. package/dist/errors.js.map +1 -0
  14. package/dist/extract-site-name.d.ts +15 -0
  15. package/dist/extract-site-name.d.ts.map +1 -0
  16. package/dist/extract-site-name.js +28 -0
  17. package/dist/extract-site-name.js.map +1 -0
  18. package/dist/graphql-client.d.ts +14 -0
  19. package/dist/graphql-client.d.ts.map +1 -0
  20. package/dist/graphql-client.js +53 -0
  21. package/dist/graphql-client.js.map +1 -0
  22. package/dist/hep-secret-provider.d.ts +18 -0
  23. package/dist/hep-secret-provider.d.ts.map +1 -0
  24. package/dist/hep-secret-provider.js +31 -0
  25. package/dist/hep-secret-provider.js.map +1 -0
  26. package/dist/index.d.ts +15 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +47 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/keycloak-auth.d.ts +50 -0
  31. package/dist/keycloak-auth.d.ts.map +1 -0
  32. package/dist/keycloak-auth.js +92 -0
  33. package/dist/keycloak-auth.js.map +1 -0
  34. package/dist/keycloak-secrets-provisioner.d.ts +44 -0
  35. package/dist/keycloak-secrets-provisioner.d.ts.map +1 -0
  36. package/dist/keycloak-secrets-provisioner.js +94 -0
  37. package/dist/keycloak-secrets-provisioner.js.map +1 -0
  38. package/dist/resilience.d.ts +26 -0
  39. package/dist/resilience.d.ts.map +1 -0
  40. package/dist/resilience.js +84 -0
  41. package/dist/resilience.js.map +1 -0
  42. package/dist/tenant-config.d.ts +23 -0
  43. package/dist/tenant-config.d.ts.map +1 -0
  44. package/dist/tenant-config.js +136 -0
  45. package/dist/tenant-config.js.map +1 -0
  46. package/dist/tls-trust.d.ts +2 -0
  47. package/dist/tls-trust.d.ts.map +1 -0
  48. package/dist/tls-trust.js +27 -0
  49. package/dist/tls-trust.js.map +1 -0
  50. package/dist/types.d.ts +40 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +3 -0
  53. package/dist/types.js.map +1 -0
  54. package/package.json +69 -0
package/README.md ADDED
@@ -0,0 +1,304 @@
1
+ # @platform-x-shp/shp-auth-client
2
+
3
+ Shared library for Platform-X's Shopify microservices — `shp-auth-service`,
4
+ `shp-cart-service`, `shp-customer-service`, `shp-order-service`, `shp-product-service`. Given a
5
+ tenant's site name (e.g. `KIWI`), it resolves that tenant's Shopify and Keycloak configuration
6
+ from a secret store, and provides three ready-to-use pieces built on top of that config:
7
+
8
+ 1. A resilient, tenant-scoped **GraphQL client** for Shopify's Admin or Storefront API.
9
+ 2. An Express **middleware that verifies a Keycloak-issued bearer token**, so every consumer
10
+ verifies tokens the same way instead of each running its own `jwks-rsa` setup.
11
+ 3. A small set of **resilience helpers** (retry, timeout, circuit breaker) and **error classes**
12
+ shared across services.
13
+
14
+ ---
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @platform-x-shp/shp-auth-client
20
+ ```
21
+
22
+ Peer dependency: `express` `^4.18.0`.
23
+
24
+ ## Quick start
25
+
26
+ ```ts
27
+ import { getTenantConfig, getGraphQLClient, gql } from '@platform-x-shp/shp-auth-client';
28
+
29
+ // Tenant config (Shopify + Keycloak settings for one site)
30
+ const config = await getTenantConfig('KIWI');
31
+ console.log(config.shopify.storeDomain); // e.g. "knma-vofh9g70.myshopify.com"
32
+
33
+ // A ready-to-use GraphQL client, pre-configured with the tenant's endpoint + auth header
34
+ const admin = await getGraphQLClient('KIWI', 'admin');
35
+ const data = await admin.request(gql`query { shop { name } }`);
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Tenant config
41
+
42
+ ### How it's resolved
43
+
44
+ Every secret is looked up under an id built as:
45
+
46
+ ```
47
+ ${SITENAME}_${PROVIDER}_${KEY} (uppercased)
48
+ ```
49
+
50
+ e.g. `buildSecretId('KIWI', 'SHOPIFY', 'STORE_DOMAIN')` → `KIWI_SHOPIFY_STORE_DOMAIN`.
51
+
52
+ ```ts
53
+ getTenantConfig(siteName: string): Promise<TenantConfig>
54
+ clearTenantCache(siteName?: string): void
55
+ setConfigProvider(provider: ConfigProvider): void // mainly for tests — see Testing below
56
+ ```
57
+
58
+ `getTenantConfig(siteName)` fetches all 18 keys for that tenant — 9 Shopify + 9 Keycloak — in
59
+ parallel, and caches the result for **5 minutes per site name** (`clearTenantCache()` to bust it
60
+ early, e.g. after rotating a secret).
61
+
62
+ **Shopify keys:** `ADMIN_API_ACCESS_TOKEN`, `IS_ENABLED`, `PLAN_NAME`, `CLIENT_ID`,
63
+ `CLIENT_SECRET`, `STORE_DOMAIN`, `STOREFRONT_API_VERSION`, `STOREFRONT_PRIVATE_TOKEN`,
64
+ `WEBHOOK_HMAC_SECRET`, `CALLBACK_URL`, `POST_LOGOUT_REDIRECT_URI`.
65
+
66
+ **Keycloak keys:** `BASE_URL` (well, `KEYCLOCK_HOST` — see below), `REALM`, `CLIENT_ID`,
67
+ `CLIENT_SECRET`, `DISCOVERY_URL`, `JWKS_URI`, `ISSUER`.
68
+
69
+ `CALLBACK_URL`/`POST_LOGOUT_REDIRECT_URI` live under the `SHOPIFY` provider segment
70
+ (`KIWI_SHOPIFY_CALLBACK_URL`, not `KIWI_KEYCLOAK_CALLBACK_URL`) even though they end up on
71
+ `TenantConfig.keycloak` — they're Shopify's external-IdP redirect URLs for this store, not
72
+ Keycloak realm data, so nothing about the realm can derive them.
73
+
74
+ Fetches are always **per-key**, never a bulk "list everything in the project" call — a GCP
75
+ project may hold hundreds of unrelated secrets, and this package only ever needs these 18 per
76
+ tenant.
77
+
78
+ ### Where secrets actually come from: `SECRET_MODE`
79
+
80
+ Each of those 18 lookups goes through [`hep-secret-access`](https://www.npmjs.com/package/hep-secret-access)'s
81
+ `secretManager()`, which picks a backing provider based on the `SECRET_MODE` env var (the same
82
+ convention `hep-delivery-service` uses):
83
+
84
+ | `SECRET_MODE` | Source | Notes |
85
+ |---|---|---|
86
+ | `env` | `process.env[secretId]` | For local dev — no GCP dependency. |
87
+ | `gsm` | GCP Secret Manager, `projects/{GOOGLE_PROJECT_ID}/secrets/{secretId}/versions/latest` | Needs `GOOGLE_PROJECT_ID` and valid GCP credentials (ADC or a service account). |
88
+ | `csi` | A file per secret under `BASEPATH` (default `/var/secrets`), named after the secret id | For a Kubernetes CSI Secret Store volume mount. |
89
+
90
+ If `SECRET_MODE` isn't set at all, `createConfigProvider('auto')` (used internally by
91
+ `getTenantConfig`) falls back based on `NODE_ENV`:
92
+
93
+ - `NODE_ENV=production` → `gsm` (throws if `GOOGLE_PROJECT_ID` is missing)
94
+ - anything else → `env`
95
+
96
+ Force a specific mode for one call without touching the ambient env:
97
+
98
+ ```ts
99
+ import { createConfigProvider, setConfigProvider } from '@platform-x-shp/shp-auth-client';
100
+
101
+ setConfigProvider(createConfigProvider('secret-manager')); // forces gsm just for this provider
102
+ ```
103
+
104
+ > **Note:** unlike `hep-secret-access`'s own `EnvProvider` (which silently returns `''` for a
105
+ > missing key), this package always throws `SecretNotFoundError` on a missing/empty secret,
106
+ > regardless of mode — a misconfigured tenant should fail loudly, not propagate blank strings.
107
+
108
+ **`GOOGLE_PROJECT_ID` vs `GCP_PROJECT_ID`:** GSM mode reads **`GOOGLE_PROJECT_ID`** — that's what
109
+ `hep-secret-access`'s `GoogleSecretManagerProvider` actually uses. It is *not* the same as
110
+ `GCP_PROJECT_ID`, which this package doesn't read at all. If `GOOGLE_PROJECT_ID` is missing while
111
+ `SECRET_MODE=gsm` is explicitly set, nothing here will catch it — GSM lookups will silently
112
+ target `hep-secret-access`'s own fallback project (`x-site-qa`) instead of yours, and fail with a
113
+ confusing "not found or no permission" error.
114
+
115
+ ### Environment variables
116
+
117
+ | Variable | Required when | Purpose |
118
+ |---|---|---|
119
+ | `SECRET_MODE` | optional | `env` \| `gsm` \| `csi` — overrides the `NODE_ENV`-based auto-selection. |
120
+ | `NODE_ENV` | used by `auto` mode | `production` → defaults to `gsm`; anything else → `env`. |
121
+ | `GOOGLE_PROJECT_ID` | `gsm` mode | GCP project id secrets are looked up in. |
122
+ | `BASEPATH` | `csi` mode | Directory secret files are mounted under (default `/var/secrets`). |
123
+ | `KIWI_SHOPIFY_*`, `KIWI_KEYCLOAK_*` | `env` mode | The 18 per-tenant values themselves (see table above), read directly from `process.env`. |
124
+
125
+ (Swap `KIWI` for whatever site name you call `getTenantConfig`/`getGraphQLClient` with — the
126
+ prefix is derived from that argument, not hardcoded.)
127
+
128
+ ---
129
+
130
+ ## GraphQL client
131
+
132
+ ```ts
133
+ getGraphQLClient(siteName: string, type?: 'admin' | 'storefront'): Promise<GraphQLClient>
134
+ clearGraphQLClientCache(siteName?: string): void
135
+ ```
136
+
137
+ Returns a cached [`graphql-request`](https://github.com/jasonkuhrt/graphql-request) client,
138
+ pre-configured per `type`:
139
+
140
+ | `type` | Endpoint | Auth header |
141
+ |---|---|---|
142
+ | `admin` (default) | `https://{storeDomain}/admin/api/{storefrontApiVersion}/graphql.json` | `X-Shopify-Access-Token: {adminApiAccessToken}` |
143
+ | `storefront` | `https://{storeDomain}/api/{storefrontApiVersion}/graphql.json` | `Shopify-Storefront-Private-Token: {storefrontPrivateToken}` |
144
+
145
+ `storefrontPrivateToken` is a **private** (server-side) Storefront token — it must go through the
146
+ `Shopify-Storefront-Private-Token` header, not the public-token header
147
+ (`X-Shopify-Storefront-Access-Token`), which Shopify rejects it with a 401.
148
+
149
+ Call `clearGraphQLClientCache(siteName?)` after rotating a token so the next call rebuilds the
150
+ client with fresh config instead of reusing the cached one.
151
+
152
+ This package also extends Node's default TLS trust store with the OS certificate store at import
153
+ time (see `tls-trust.ts`), so `getGraphQLClient` calls succeed behind a TLS-intercepting corporate
154
+ proxy without every consuming service needing its own `--use-system-ca` flag or CA bundle file.
155
+
156
+ ## Resilience helpers
157
+
158
+ Building blocks for wrapping outbound calls — used internally by services that call
159
+ `getGraphQLClient`, also exported for direct use:
160
+
161
+ ```ts
162
+ withRetry(fn, retries = 3, baseMs = 200, shouldRetry?): Promise<T> // exponential backoff
163
+ withTimeout(fn, ms = 5000): Promise<T> // throws TimeoutError
164
+ new CircuitBreaker(failureThreshold = 5, resetAfterMs = 30_000) // .call(fn), .isOpen(), .reset()
165
+ ```
166
+
167
+ Typical usage in a consuming service:
168
+
169
+ ```ts
170
+ const breaker = new CircuitBreaker();
171
+
172
+ async function adminGraphql<T>(query: string, variables?: Record<string, unknown>): Promise<T> {
173
+ return breaker.call(() =>
174
+ withTimeout(() =>
175
+ withRetry(async () => {
176
+ const client = await getGraphQLClient('KIWI', 'admin');
177
+ return client.request<T>(query, variables);
178
+ }, 3, 300, (err) => err instanceof ClientError && (err.response?.status === 429 || (err.response?.status ?? 0) >= 500)),
179
+ 15000
180
+ )
181
+ );
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Keycloak token verification
188
+
189
+ ```ts
190
+ extractBearer(authorization?: string): string
191
+ verifyKeycloakToken(token: string, config: KeycloakVerifyConfig): Promise<KeycloakTokenPayload>
192
+ keycloakConfigForTenant(siteName: string, audience?: string): Promise<KeycloakVerifyConfig>
193
+ requireKeycloakAuth(config: KeycloakVerifyConfig | ((req) => KeycloakVerifyConfig | Promise<KeycloakVerifyConfig>))
194
+ ```
195
+
196
+ - `extractBearer` strips the `Bearer ` prefix from an `Authorization` header, throwing
197
+ `UnauthorizedError` if it's missing or malformed.
198
+ - `verifyKeycloakToken` checks a token's RS256 signature (via `config.jwksUri`), issuer, and — if
199
+ given — audience. One `jwks-rsa` client is created per distinct `jwksUri` and reused across
200
+ calls (it has its own internal signing-key cache + rate limiter), so calling this repeatedly for
201
+ the same tenant doesn't refetch keys.
202
+ - `keycloakConfigForTenant(siteName, audience?)` builds a `KeycloakVerifyConfig` from
203
+ `getTenantConfig(siteName).keycloak` — the issuer/JWKS come from the tenant's resolved config,
204
+ not a locally duplicated env var. `audience` is passed through as-is since it's a per-caller
205
+ restriction, not a per-tenant secret.
206
+ - `requireKeycloakAuth` is the Express middleware most services actually use. It takes either a
207
+ static `KeycloakVerifyConfig` (a service with its own fixed issuer/JWKS env vars — e.g.
208
+ `shp-auth-service`'s admin-facing Keycloak config) or a function that resolves one per request —
209
+ typically `() => keycloakConfigForTenant(SITE_NAME)`. On success it sets `req.user = { sub,
210
+ role }` and `req.keycloakUser` (the full decoded payload) before calling `next()`.
211
+
212
+ ```ts
213
+ import { requireKeycloakAuth, keycloakConfigForTenant } from '@platform-x-shp/shp-auth-client';
214
+
215
+ export const requireAuth = requireKeycloakAuth(() =>
216
+ keycloakConfigForTenant('KIWI', process.env.KEYCLOAK_AUDIENCE || undefined),
217
+ );
218
+
219
+ router.get('/orders/:id', requireAuth, ordersController.getOrder);
220
+ ```
221
+
222
+ Failures reject with `TokenExpiredError`, `TokenInvalidError`, or `UnauthorizedError` (see Errors
223
+ below) — `requireKeycloakAuth` forwards them to `next(err)`, so a service's own
224
+ `globalErrorHandler` (or this package's) maps them to the right HTTP status.
225
+
226
+ ---
227
+
228
+ ## Errors
229
+
230
+ All thrown as plain `Error` subclasses with a distinct `.name`:
231
+
232
+ | Class | Meaning |
233
+ |---|---|
234
+ | `TenantNotFoundError` | Tenant config not found for a site name |
235
+ | `SecretNotFoundError` | A specific secret was missing/empty |
236
+ | `TokenExpiredError` / `TokenInvalidError` | Keycloak token verification failures |
237
+ | `UnauthorizedError` / `ForbiddenError` / `CrossTenantAccessError` | Access control |
238
+ | `ShopifyApiError` / `ShopifyUserError` | Shopify API / GraphQL user errors |
239
+ | `TimeoutError` / `CircuitOpenError` | From `withTimeout` / `CircuitBreaker` |
240
+
241
+ `globalErrorHandler(err, req, res, next)` is an optional Express error-handling middleware that
242
+ maps each of the above to an HTTP status code and returns `{ error, code }` — use it directly, or
243
+ as a reference for a service's own error handler.
244
+
245
+ ## Multi-tenancy from a request (optional)
246
+
247
+ ```ts
248
+ extractSiteName(req): string | null // 'sitehost'/'sitename' header, or hostname, uppercased
249
+ ```
250
+
251
+ Not currently wired into any consuming service's request pipeline (each service today resolves a
252
+ single fixed `SITE_NAME` from its own env at startup) — provided for a future per-request,
253
+ multi-tenant routing scenario.
254
+
255
+ ---
256
+
257
+ ## Testing
258
+
259
+ `setConfigProvider(provider)` swaps the module-level `ConfigProvider` `getTenantConfig` uses (and
260
+ clears the tenant cache) — pass a mock implementing `{ get(secretId): Promise<string> }` in tests
261
+ instead of hitting `env`/`gsm`/`csi` for real.
262
+
263
+ ```bash
264
+ npm test # cross-env NODE_ENV=test jest --runInBand
265
+ npm run typecheck
266
+ npm run lint
267
+ ```
268
+
269
+ ## Full API surface
270
+
271
+ ```ts
272
+ export { getTenantConfig, clearTenantCache, setConfigProvider };
273
+ export { extractSiteName, buildSecretId };
274
+ export { createConfigProvider };
275
+ export type { ProviderMode };
276
+ export { HepSecretProvider };
277
+ export { TTLCache };
278
+ export { getGraphQLClient, clearGraphQLClientCache, gql, ClientError };
279
+ export { withRetry, withTimeout, CircuitBreaker };
280
+ export { extractBearer, verifyKeycloakToken, keycloakConfigForTenant, requireKeycloakAuth };
281
+ export type { KeycloakTokenPayload, KeycloakVerifyConfig };
282
+ export {
283
+ TenantNotFoundError, SecretNotFoundError,
284
+ TokenExpiredError, TokenInvalidError, UnauthorizedError,
285
+ CrossTenantAccessError, ForbiddenError,
286
+ ShopifyApiError, ShopifyUserError,
287
+ TimeoutError, CircuitOpenError,
288
+ globalErrorHandler,
289
+ };
290
+ export type { TenantConfig, ShopifyConfig, KeycloakConfig, ConfigProvider };
291
+ ```
292
+
293
+ ## Development
294
+
295
+ ```bash
296
+ npm run build # tsc -> dist/
297
+ npm run dev # tsc --watch
298
+ npm run clean # rm -rf dist
299
+ ```
300
+
301
+ Bump `version` in `package.json` and `npm publish` (registry: `registry.npmjs.org`, scope
302
+ `@platform-x` per `.npmrc`) to ship changes to consuming services — they pin
303
+ `@platform-x-shp/shp-auth-client` by version, so a local change here has no effect on them until
304
+ published and their dependency is bumped + reinstalled.
@@ -0,0 +1,10 @@
1
+ export declare class TTLCache<T> {
2
+ private store;
3
+ private ttlMs;
4
+ constructor(ttlMs?: number);
5
+ get(key: string): T | null;
6
+ set(key: string, value: T): void;
7
+ delete(key: string): void;
8
+ clear(): void;
9
+ }
10
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAKA,qBAAa,QAAQ,CAAC,CAAC;IACrB,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,KAAK,CAAS;gBAEV,KAAK,SAAgB;IAIjC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI;IAS1B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IAIhC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,KAAK,IAAI,IAAI;CAGd"}
package/dist/cache.js ADDED
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TTLCache = void 0;
4
+ class TTLCache {
5
+ constructor(ttlMs = 5 * 60 * 1000) {
6
+ this.store = new Map();
7
+ this.ttlMs = ttlMs;
8
+ }
9
+ get(key) {
10
+ const entry = this.store.get(key);
11
+ if (!entry || Date.now() > entry.expiresAt) {
12
+ this.store.delete(key);
13
+ return null;
14
+ }
15
+ return entry.value;
16
+ }
17
+ set(key, value) {
18
+ this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });
19
+ }
20
+ delete(key) {
21
+ this.store.delete(key);
22
+ }
23
+ clear() {
24
+ this.store.clear();
25
+ }
26
+ }
27
+ exports.TTLCache = TTLCache;
28
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":";;;AAKA,MAAa,QAAQ;IAInB,YAAY,KAAK,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI;QAHzB,UAAK,GAAG,IAAI,GAAG,EAAyB,CAAC;QAI/C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAED,GAAG,CAAC,GAAW;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;YAC3C,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,KAAQ;QACvB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IACrE,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;CACF;AA5BD,4BA4BC"}
@@ -0,0 +1,17 @@
1
+ import type { ConfigProvider } from './types';
2
+ export type ProviderMode = 'secret-manager' | 'env' | 'csi' | 'auto';
3
+ /**
4
+ * Factory to create the appropriate config provider. Secrets are actually
5
+ * resolved by hep-secret-access's secretManager() (env | gsm | csi, chosen via
6
+ * SECRET_MODE — see hep-delivery-service's secret_key.services.ts for the
7
+ * reference usage this mirrors).
8
+ *
9
+ * An explicit mode forces that SECRET_MODE (set just before constructing the
10
+ * provider, then restored) regardless of the ambient value. 'auto' (the
11
+ * default) defers to whatever SECRET_MODE is already set; only when it isn't
12
+ * set at all does this fall back to NODE_ENV-based selection (gsm in
13
+ * production, env otherwise) so deployments that never set SECRET_MODE keep
14
+ * working unchanged.
15
+ */
16
+ export declare function createConfigProvider(mode?: ProviderMode): ConfigProvider;
17
+ //# sourceMappingURL=config-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-provider.d.ts","sourceRoot":"","sources":["../src/config-provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,YAAY,GAAG,gBAAgB,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AASrE;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,YAAqB,GAAG,cAAc,CAyBhF"}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createConfigProvider = createConfigProvider;
4
+ const hep_secret_provider_1 = require("./hep-secret-provider");
5
+ /** Our mode names -> hep-secret-access's SECRET_MODE values. */
6
+ const TO_HEP_MODE = {
7
+ env: 'env',
8
+ 'secret-manager': 'gsm',
9
+ csi: 'csi',
10
+ };
11
+ /**
12
+ * Factory to create the appropriate config provider. Secrets are actually
13
+ * resolved by hep-secret-access's secretManager() (env | gsm | csi, chosen via
14
+ * SECRET_MODE — see hep-delivery-service's secret_key.services.ts for the
15
+ * reference usage this mirrors).
16
+ *
17
+ * An explicit mode forces that SECRET_MODE (set just before constructing the
18
+ * provider, then restored) regardless of the ambient value. 'auto' (the
19
+ * default) defers to whatever SECRET_MODE is already set; only when it isn't
20
+ * set at all does this fall back to NODE_ENV-based selection (gsm in
21
+ * production, env otherwise) so deployments that never set SECRET_MODE keep
22
+ * working unchanged.
23
+ */
24
+ function createConfigProvider(mode = 'auto') {
25
+ if (mode !== 'auto') {
26
+ const previous = process.env.SECRET_MODE;
27
+ process.env.SECRET_MODE = TO_HEP_MODE[mode];
28
+ try {
29
+ return new hep_secret_provider_1.HepSecretProvider();
30
+ }
31
+ finally {
32
+ if (previous === undefined)
33
+ delete process.env.SECRET_MODE;
34
+ else
35
+ process.env.SECRET_MODE = previous;
36
+ }
37
+ }
38
+ if (!process.env.SECRET_MODE) {
39
+ if (process.env.NODE_ENV === 'production') {
40
+ // GOOGLE_PROJECT_ID (not GCP_PROJECT_ID) is what hep-secret-access's
41
+ // GoogleSecretManagerProvider actually reads — checking the wrong name
42
+ // here would let this pass while GSM silently falls back to its own
43
+ // default project.
44
+ if (!process.env.GOOGLE_PROJECT_ID)
45
+ throw new Error('GOOGLE_PROJECT_ID is required in production');
46
+ process.env.SECRET_MODE = 'gsm';
47
+ }
48
+ else {
49
+ process.env.SECRET_MODE = 'env';
50
+ }
51
+ }
52
+ return new hep_secret_provider_1.HepSecretProvider();
53
+ }
54
+ //# sourceMappingURL=config-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-provider.js","sourceRoot":"","sources":["../src/config-provider.ts"],"names":[],"mappings":";;AAyBA,oDAyBC;AAlDD,+DAA0D;AAK1D,gEAAgE;AAChE,MAAM,WAAW,GAAqD;IACpE,GAAG,EAAE,KAAK;IACV,gBAAgB,EAAE,KAAK;IACvB,GAAG,EAAE,KAAK;CACX,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,SAAgB,oBAAoB,CAAC,OAAqB,MAAM;IAC9D,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;QACzC,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,CAAC;YACH,OAAO,IAAI,uCAAiB,EAAE,CAAC;QACjC,CAAC;gBAAS,CAAC;YACT,IAAI,QAAQ,KAAK,SAAS;gBAAE,OAAO,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;;gBACtD,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,QAAQ,CAAC;QAC1C,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;QAC7B,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;YAC1C,qEAAqE;YACrE,uEAAuE;YACvE,oEAAoE;YACpE,mBAAmB;YACnB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB;gBAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;YACnG,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,KAAK,CAAC;QAClC,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,KAAK,CAAC;QAClC,CAAC;IACH,CAAC;IACD,OAAO,IAAI,uCAAiB,EAAE,CAAC;AACjC,CAAC"}
@@ -0,0 +1,53 @@
1
+ export declare class TenantNotFoundError extends Error {
2
+ constructor(siteName: string);
3
+ }
4
+ export declare class SecretNotFoundError extends Error {
5
+ constructor(secretId: string);
6
+ }
7
+ export declare class TokenExpiredError extends Error {
8
+ constructor();
9
+ }
10
+ export declare class TokenInvalidError extends Error {
11
+ constructor(detail?: string);
12
+ }
13
+ export declare class UnauthorizedError extends Error {
14
+ constructor(message?: string);
15
+ }
16
+ export declare class CrossTenantAccessError extends Error {
17
+ constructor();
18
+ }
19
+ export declare class ForbiddenError extends Error {
20
+ constructor(message?: string);
21
+ }
22
+ export declare class ShopifyApiError extends Error {
23
+ readonly statusCode: number;
24
+ readonly body: unknown;
25
+ constructor(statusCode: number, body: unknown);
26
+ }
27
+ export declare class ShopifyUserError extends Error {
28
+ readonly errors: Array<{
29
+ field: string[];
30
+ message: string;
31
+ }>;
32
+ constructor(errors: Array<{
33
+ field: string[];
34
+ message: string;
35
+ }>);
36
+ }
37
+ export declare class TimeoutError extends Error {
38
+ constructor(message: string);
39
+ }
40
+ export declare class CircuitOpenError extends Error {
41
+ constructor(message: string);
42
+ }
43
+ export interface ErrorHandlerRequest {
44
+ headers: Record<string, string | string[] | undefined>;
45
+ tenantId?: string;
46
+ }
47
+ export interface ErrorHandlerResponse {
48
+ status(code: number): {
49
+ json(body: unknown): unknown;
50
+ };
51
+ }
52
+ export declare function globalErrorHandler(err: Error, req: ErrorHandlerRequest, res: ErrorHandlerResponse, _next: unknown): void;
53
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,QAAQ,EAAE,MAAM;CAI7B;AAED,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,QAAQ,EAAE,MAAM;CAI7B;AAID,qBAAa,iBAAkB,SAAQ,KAAK;;CAK3C;AAED,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,MAAM,CAAC,EAAE,MAAM;CAI5B;AAED,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,SAAiB;CAIrC;AAID,qBAAa,sBAAuB,SAAQ,KAAK;;CAKhD;AAED,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,SAAc;CAIlC;AAID,qBAAa,eAAgB,SAAQ,KAAK;aAEtB,UAAU,EAAE,MAAM;aAClB,IAAI,EAAE,OAAO;gBADb,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,OAAO;CAKhC;AAED,qBAAa,gBAAiB,SAAQ,KAAK;aACb,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;gBAAnD,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAIhF;AAID,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAiBD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;KAAE,CAAC;CACxD;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,KAAK,EACV,GAAG,EAAE,mBAAmB,EACxB,GAAG,EAAE,oBAAoB,EAIzB,KAAK,EAAE,OAAO,GACb,IAAI,CAaN"}
package/dist/errors.js ADDED
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ // ─── Tenant / Config errors ─────────────────────────────────────────────────
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.CircuitOpenError = exports.TimeoutError = exports.ShopifyUserError = exports.ShopifyApiError = exports.ForbiddenError = exports.CrossTenantAccessError = exports.UnauthorizedError = exports.TokenInvalidError = exports.TokenExpiredError = exports.SecretNotFoundError = exports.TenantNotFoundError = void 0;
5
+ exports.globalErrorHandler = globalErrorHandler;
6
+ class TenantNotFoundError extends Error {
7
+ constructor(siteName) {
8
+ super(`Tenant config not found: ${siteName}`);
9
+ this.name = 'TenantNotFoundError';
10
+ }
11
+ }
12
+ exports.TenantNotFoundError = TenantNotFoundError;
13
+ class SecretNotFoundError extends Error {
14
+ constructor(secretId) {
15
+ super(`Secret not found or empty: ${secretId}`);
16
+ this.name = 'SecretNotFoundError';
17
+ }
18
+ }
19
+ exports.SecretNotFoundError = SecretNotFoundError;
20
+ // ─── Auth / Token errors ───────────────────────────────────────────────────
21
+ class TokenExpiredError extends Error {
22
+ constructor() {
23
+ super('Access token has expired');
24
+ this.name = 'TokenExpiredError';
25
+ }
26
+ }
27
+ exports.TokenExpiredError = TokenExpiredError;
28
+ class TokenInvalidError extends Error {
29
+ constructor(detail) {
30
+ super(detail ? `Invalid token: ${detail}` : 'Invalid token');
31
+ this.name = 'TokenInvalidError';
32
+ }
33
+ }
34
+ exports.TokenInvalidError = TokenInvalidError;
35
+ class UnauthorizedError extends Error {
36
+ constructor(message = 'Unauthorized') {
37
+ super(message);
38
+ this.name = 'UnauthorizedError';
39
+ }
40
+ }
41
+ exports.UnauthorizedError = UnauthorizedError;
42
+ // ─── Access control errors ─────────────────────────────────────────────────
43
+ class CrossTenantAccessError extends Error {
44
+ constructor() {
45
+ super('Cross-tenant access denied');
46
+ this.name = 'CrossTenantAccessError';
47
+ }
48
+ }
49
+ exports.CrossTenantAccessError = CrossTenantAccessError;
50
+ class ForbiddenError extends Error {
51
+ constructor(message = 'Forbidden') {
52
+ super(message);
53
+ this.name = 'ForbiddenError';
54
+ }
55
+ }
56
+ exports.ForbiddenError = ForbiddenError;
57
+ // ─── Shopify API errors ────────────────────────────────────────────────────
58
+ class ShopifyApiError extends Error {
59
+ constructor(statusCode, body) {
60
+ super(`Shopify API error ${statusCode}`);
61
+ this.statusCode = statusCode;
62
+ this.body = body;
63
+ this.name = 'ShopifyApiError';
64
+ }
65
+ }
66
+ exports.ShopifyApiError = ShopifyApiError;
67
+ class ShopifyUserError extends Error {
68
+ constructor(errors) {
69
+ super(errors.map((e) => e.message).join(', '));
70
+ this.errors = errors;
71
+ this.name = 'ShopifyUserError';
72
+ }
73
+ }
74
+ exports.ShopifyUserError = ShopifyUserError;
75
+ // ─── Resilience errors ─────────────────────────────────────────────────────
76
+ class TimeoutError extends Error {
77
+ constructor(message) {
78
+ super(message);
79
+ this.name = 'TimeoutError';
80
+ }
81
+ }
82
+ exports.TimeoutError = TimeoutError;
83
+ class CircuitOpenError extends Error {
84
+ constructor(message) {
85
+ super(message);
86
+ this.name = 'CircuitOpenError';
87
+ }
88
+ }
89
+ exports.CircuitOpenError = CircuitOpenError;
90
+ // ─── Global Express error handler ────────────────────────────────────────────
91
+ const HTTP_STATUS_MAP = {
92
+ TenantNotFoundError: 404,
93
+ SecretNotFoundError: 404,
94
+ TokenExpiredError: 401,
95
+ TokenInvalidError: 401,
96
+ UnauthorizedError: 401,
97
+ CrossTenantAccessError: 403,
98
+ ForbiddenError: 403,
99
+ ShopifyUserError: 422,
100
+ TimeoutError: 504,
101
+ CircuitOpenError: 503,
102
+ };
103
+ function globalErrorHandler(err, req, res,
104
+ // Express only recognizes error-handling middleware by its 4-arg arity —
105
+ // _next must stay in the signature even though it's never called.
106
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
107
+ _next) {
108
+ const sitehostHeader = req.headers['sitehost'];
109
+ const siteName = req.tenantId ?? (Array.isArray(sitehostHeader) ? sitehostHeader[0] : sitehostHeader) ?? 'unknown';
110
+ console.error(`[${siteName}] ${err.name}: ${err.message}`);
111
+ const status = HTTP_STATUS_MAP[err.name] ?? 500;
112
+ const message = status === 500
113
+ ? 'Internal server error'
114
+ : err.message;
115
+ res.status(status).json({ error: message, code: err.name });
116
+ }
117
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";AAAA,+EAA+E;;;AAkH/E,gDAqBC;AArID,MAAa,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,QAAgB;QAC1B,KAAK,CAAC,4BAA4B,QAAQ,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AALD,kDAKC;AAED,MAAa,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,QAAgB;QAC1B,KAAK,CAAC,8BAA8B,QAAQ,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AALD,kDAKC;AAED,8EAA8E;AAE9E,MAAa,iBAAkB,SAAQ,KAAK;IAC1C;QACE,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAClC,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AALD,8CAKC;AAED,MAAa,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,MAAe;QACzB,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,kBAAkB,MAAM,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AALD,8CAKC;AAED,MAAa,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAO,GAAG,cAAc;QAClC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AALD,8CAKC;AAED,8EAA8E;AAE9E,MAAa,sBAAuB,SAAQ,KAAK;IAC/C;QACE,KAAK,CAAC,4BAA4B,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AALD,wDAKC;AAED,MAAa,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAO,GAAG,WAAW;QAC/B,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AALD,wCAKC;AAED,8EAA8E;AAE9E,MAAa,eAAgB,SAAQ,KAAK;IACxC,YACkB,UAAkB,EAClB,IAAa;QAE7B,KAAK,CAAC,qBAAqB,UAAU,EAAE,CAAC,CAAC;QAHzB,eAAU,GAAV,UAAU,CAAQ;QAClB,SAAI,GAAJ,IAAI,CAAS;QAG7B,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AARD,0CAQC;AAED,MAAa,gBAAiB,SAAQ,KAAK;IACzC,YAA4B,MAAmD;QAC7E,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QADrB,WAAM,GAAN,MAAM,CAA6C;QAE7E,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AALD,4CAKC;AAED,8EAA8E;AAE9E,MAAa,YAAa,SAAQ,KAAK;IACrC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF;AALD,oCAKC;AAED,MAAa,gBAAiB,SAAQ,KAAK;IACzC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AALD,4CAKC;AAED,gFAAgF;AAEhF,MAAM,eAAe,GAA2B;IAC9C,mBAAmB,EAAE,GAAG;IACxB,mBAAmB,EAAE,GAAG;IACxB,iBAAiB,EAAE,GAAG;IACtB,iBAAiB,EAAE,GAAG;IACtB,iBAAiB,EAAE,GAAG;IACtB,sBAAsB,EAAE,GAAG;IAC3B,cAAc,EAAE,GAAG;IACnB,gBAAgB,EAAE,GAAG;IACrB,YAAY,EAAE,GAAG;IACjB,gBAAgB,EAAE,GAAG;CACtB,CAAC;AAWF,SAAgB,kBAAkB,CAChC,GAAU,EACV,GAAwB,EACxB,GAAyB;AACzB,yEAAyE;AACzE,kEAAkE;AAClE,6DAA6D;AAC7D,KAAc;IAEd,MAAM,cAAc,GAAG,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/C,MAAM,QAAQ,GACZ,GAAG,CAAC,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,SAAS,CAAC;IACpG,OAAO,CAAC,KAAK,CAAC,IAAI,QAAQ,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IAE3D,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC;IAChD,MAAM,OAAO,GACX,MAAM,KAAK,GAAG;QACZ,CAAC,CAAC,uBAAuB;QACzB,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC;IAElB,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { Request } from 'express';
2
+ /**
3
+ * Extracts and normalizes site name from request headers.
4
+ * Priority: sitehost > sitename > hostname
5
+ * Header 'sitehost: kiwi.hcl-x.com' => 'KIWI'
6
+ * Used to build Secret Manager keys: KIWI_SHOPIFY_STORE_DOMAIN
7
+ */
8
+ export declare function extractSiteName(req: Request): string | null;
9
+ /**
10
+ * Builds a Secret Manager secret ID using the convention:
11
+ * ${SITENAME}_${PROVIDER}_${KEY}
12
+ * e.g. buildSecretId('KIWI', 'SHOPIFY', 'STORE_DOMAIN') => 'KIWI_SHOPIFY_STORE_DOMAIN'
13
+ */
14
+ export declare function buildSecretId(siteName: string, provider: string, key: string): string;
15
+ //# sourceMappingURL=extract-site-name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-site-name.d.ts","sourceRoot":"","sources":["../src/extract-site-name.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAU3D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErF"}
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractSiteName = extractSiteName;
4
+ exports.buildSecretId = buildSecretId;
5
+ /**
6
+ * Extracts and normalizes site name from request headers.
7
+ * Priority: sitehost > sitename > hostname
8
+ * Header 'sitehost: kiwi.hcl-x.com' => 'KIWI'
9
+ * Used to build Secret Manager keys: KIWI_SHOPIFY_STORE_DOMAIN
10
+ */
11
+ function extractSiteName(req) {
12
+ const siteHeader = req.headers['sitehost'] ||
13
+ req.headers['sitename'] ||
14
+ req.hostname;
15
+ if (!siteHeader)
16
+ return null;
17
+ // 'kiwi.hcl-x.com' => 'KIWI'
18
+ return siteHeader.split('.')[0].toUpperCase();
19
+ }
20
+ /**
21
+ * Builds a Secret Manager secret ID using the convention:
22
+ * ${SITENAME}_${PROVIDER}_${KEY}
23
+ * e.g. buildSecretId('KIWI', 'SHOPIFY', 'STORE_DOMAIN') => 'KIWI_SHOPIFY_STORE_DOMAIN'
24
+ */
25
+ function buildSecretId(siteName, provider, key) {
26
+ return `${siteName}_${provider}_${key}`.toUpperCase();
27
+ }
28
+ //# sourceMappingURL=extract-site-name.js.map