@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.
- package/README.md +304 -0
- package/dist/cache.d.ts +10 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +28 -0
- package/dist/cache.js.map +1 -0
- package/dist/config-provider.d.ts +17 -0
- package/dist/config-provider.d.ts.map +1 -0
- package/dist/config-provider.js +54 -0
- package/dist/config-provider.js.map +1 -0
- package/dist/errors.d.ts +53 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +117 -0
- package/dist/errors.js.map +1 -0
- package/dist/extract-site-name.d.ts +15 -0
- package/dist/extract-site-name.d.ts.map +1 -0
- package/dist/extract-site-name.js +28 -0
- package/dist/extract-site-name.js.map +1 -0
- package/dist/graphql-client.d.ts +14 -0
- package/dist/graphql-client.d.ts.map +1 -0
- package/dist/graphql-client.js +53 -0
- package/dist/graphql-client.js.map +1 -0
- package/dist/hep-secret-provider.d.ts +18 -0
- package/dist/hep-secret-provider.d.ts.map +1 -0
- package/dist/hep-secret-provider.js +31 -0
- package/dist/hep-secret-provider.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/keycloak-auth.d.ts +50 -0
- package/dist/keycloak-auth.d.ts.map +1 -0
- package/dist/keycloak-auth.js +92 -0
- package/dist/keycloak-auth.js.map +1 -0
- package/dist/keycloak-secrets-provisioner.d.ts +44 -0
- package/dist/keycloak-secrets-provisioner.d.ts.map +1 -0
- package/dist/keycloak-secrets-provisioner.js +94 -0
- package/dist/keycloak-secrets-provisioner.js.map +1 -0
- package/dist/resilience.d.ts +26 -0
- package/dist/resilience.d.ts.map +1 -0
- package/dist/resilience.js +84 -0
- package/dist/resilience.js.map +1 -0
- package/dist/tenant-config.d.ts +23 -0
- package/dist/tenant-config.d.ts.map +1 -0
- package/dist/tenant-config.js +136 -0
- package/dist/tenant-config.js.map +1 -0
- package/dist/tls-trust.d.ts +2 -0
- package/dist/tls-trust.d.ts.map +1 -0
- package/dist/tls-trust.js +27 -0
- package/dist/tls-trust.js.map +1 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- 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.
|
package/dist/cache.d.ts
ADDED
|
@@ -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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -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":"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"}
|
|
@@ -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
|