@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
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CircuitBreaker = void 0;
4
+ exports.withRetry = withRetry;
5
+ exports.withTimeout = withTimeout;
6
+ const errors_1 = require("./errors");
7
+ /**
8
+ * Retries an async operation with exponential back-off.
9
+ *
10
+ * @param fn - Async function to execute
11
+ * @param retries - Maximum retry attempts (default 3)
12
+ * @param baseMs - Initial delay in ms (default 200); doubles each attempt
13
+ * @param shouldRetry - Optional predicate; returning false stops retrying early
14
+ */
15
+ async function withRetry(fn, retries = 3, baseMs = 200, shouldRetry) {
16
+ let lastError;
17
+ for (let attempt = 0; attempt <= retries; attempt++) {
18
+ try {
19
+ return await fn();
20
+ }
21
+ catch (err) {
22
+ lastError = err;
23
+ if (attempt === retries)
24
+ break;
25
+ if (shouldRetry && !shouldRetry(err))
26
+ break;
27
+ await sleep(baseMs * 2 ** attempt);
28
+ }
29
+ }
30
+ throw lastError;
31
+ }
32
+ /** Wraps an async function with a timeout. Throws if it takes longer than ms. */
33
+ async function withTimeout(fn, ms = 5000) {
34
+ return Promise.race([
35
+ fn(),
36
+ new Promise((_, reject) => setTimeout(() => reject(new errors_1.TimeoutError(`Operation timed out after ${ms}ms`)), ms)),
37
+ ]);
38
+ }
39
+ /**
40
+ * Simple circuit breaker. Opens after `failureThreshold` consecutive failures
41
+ * and resets after `resetAfterMs`.
42
+ */
43
+ class CircuitBreaker {
44
+ constructor(failureThreshold = 5, resetAfterMs = 30000) {
45
+ this.failureThreshold = failureThreshold;
46
+ this.resetAfterMs = resetAfterMs;
47
+ this.failures = 0;
48
+ this.openSince = null;
49
+ }
50
+ isOpen() {
51
+ if (this.openSince === null)
52
+ return false;
53
+ if (Date.now() - this.openSince >= this.resetAfterMs) {
54
+ this.failures = 0;
55
+ this.openSince = null;
56
+ return false; // half-open: allow one attempt
57
+ }
58
+ return true;
59
+ }
60
+ async call(fn) {
61
+ if (this.isOpen())
62
+ throw new errors_1.CircuitOpenError('Circuit breaker is open');
63
+ try {
64
+ const result = await fn();
65
+ this.failures = 0;
66
+ return result;
67
+ }
68
+ catch (err) {
69
+ this.failures++;
70
+ if (this.failures >= this.failureThreshold)
71
+ this.openSince = Date.now();
72
+ throw err;
73
+ }
74
+ }
75
+ reset() {
76
+ this.failures = 0;
77
+ this.openSince = null;
78
+ }
79
+ }
80
+ exports.CircuitBreaker = CircuitBreaker;
81
+ function sleep(ms) {
82
+ return new Promise((resolve) => setTimeout(resolve, ms));
83
+ }
84
+ //# sourceMappingURL=resilience.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resilience.js","sourceRoot":"","sources":["../src/resilience.ts"],"names":[],"mappings":";;;AAUA,8BAkBC;AAGD,kCAOC;AAtCD,qCAA0D;AAE1D;;;;;;;GAOG;AACI,KAAK,UAAU,SAAS,CAC7B,EAAoB,EACpB,OAAO,GAAG,CAAC,EACX,MAAM,GAAG,GAAG,EACZ,WAAuC;IAEvC,IAAI,SAAkB,CAAC;IACvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;QACpD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,SAAS,GAAG,GAAG,CAAC;YAChB,IAAI,OAAO,KAAK,OAAO;gBAAE,MAAM;YAC/B,IAAI,WAAW,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC;gBAAE,MAAM;YAC5C,MAAM,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IACD,MAAM,SAAS,CAAC;AAClB,CAAC;AAED,iFAAiF;AAC1E,KAAK,UAAU,WAAW,CAAI,EAAoB,EAAE,EAAE,GAAG,IAAI;IAClE,OAAO,OAAO,CAAC,IAAI,CAAC;QAClB,EAAE,EAAE;QACJ,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAC/B,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,qBAAY,CAAC,6BAA6B,EAAE,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC,CACpF;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAa,cAAc;IAIzB,YACmB,mBAAmB,CAAC,EACpB,eAAe,KAAM;QADrB,qBAAgB,GAAhB,gBAAgB,CAAI;QACpB,iBAAY,GAAZ,YAAY,CAAS;QALhC,aAAQ,GAAG,CAAC,CAAC;QACb,cAAS,GAAkB,IAAI,CAAC;IAKrC,CAAC;IAEJ,MAAM;QACJ,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC1C,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACrD,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;YAClB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,OAAO,KAAK,CAAC,CAAC,+BAA+B;QAC/C,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,IAAI,CAAI,EAAoB;QAChC,IAAI,IAAI,CAAC,MAAM,EAAE;YAAE,MAAM,IAAI,yBAAgB,CAAC,yBAAyB,CAAC,CAAC;QACzE,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,EAAE,EAAE,CAAC;YAC1B,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;YAClB,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,gBAAgB;gBAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACxE,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,KAAK;QACH,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;QAClB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;IACxB,CAAC;CACF;AApCD,wCAoCC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { ConfigProvider, TenantConfig, TenantAuthInfo } from './types';
2
+ /** Override the config provider (useful for testing with mocks). */
3
+ export declare function setConfigProvider(p: ConfigProvider): void;
4
+ /**
5
+ * Main entry point. Fetches and caches tenant config for 5 minutes.
6
+ * @param siteName - Normalized site name e.g. 'KIWI'
7
+ */
8
+ export declare function getTenantConfig(siteName: string): Promise<TenantConfig>;
9
+ /** Clear cached config for a tenant or all tenants. */
10
+ export declare function clearTenantCache(siteName?: string): void;
11
+ /**
12
+ * Looks up a site's entry in the AUTH_TENANTS_INFO secret (a base64-encoded
13
+ * JSON array). Returns `null` when the site isn't listed there, meaning it
14
+ * uses the shared realm rather than a dedicated one.
15
+ *
16
+ * Case-insensitive: `buildSecretId` already normalizes site names to
17
+ * uppercase for secret ids, so a caller passing e.g. 'kiwi' while
18
+ * AUTH_TENANTS_INFO stores 'KIWI' (or vice versa) must still match here —
19
+ * otherwise this silently returns null and the caller sees a confusing
20
+ * "missing tenant/client data" error instead of the real entry.
21
+ */
22
+ export declare function getClientTenantDetails(siteName: string): Promise<TenantAuthInfo | null>;
23
+ //# sourceMappingURL=tenant-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-config.d.ts","sourceRoot":"","sources":["../src/tenant-config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAiC,cAAc,EAAE,MAAM,SAAS,CAAC;AAY3G,oEAAoE;AACpE,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,cAAc,GAAG,IAAI,CAIzD;AAqED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAU7E;AAED,uDAAuD;AACvD,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAOxD;AAaD;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAI7F"}
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setConfigProvider = setConfigProvider;
4
+ exports.getTenantConfig = getTenantConfig;
5
+ exports.clearTenantCache = clearTenantCache;
6
+ exports.getClientTenantDetails = getClientTenantDetails;
7
+ const cache_1 = require("./cache");
8
+ const config_provider_1 = require("./config-provider");
9
+ const errors_1 = require("./errors");
10
+ const extract_site_name_1 = require("./extract-site-name");
11
+ const cache = new cache_1.TTLCache();
12
+ let provider = (0, config_provider_1.createConfigProvider)('auto');
13
+ // Single global secret (not per-site, so it bypasses buildSecretId) listing
14
+ // every dedicated-realm tenant. Cached under one fixed key since it's fetched
15
+ // and parsed as a whole rather than per site. Mirrors hep-delivery-service's
16
+ // SECRET_KEYS.AUTH_TENANTS_INFO / getClientTenantDetails (user_auth.services.ts).
17
+ const AUTH_TENANTS_INFO_SECRET_ID = 'AUTH_TENANTS_INFO';
18
+ const authTenantsCache = new cache_1.TTLCache();
19
+ /** Override the config provider (useful for testing with mocks). */
20
+ function setConfigProvider(p) {
21
+ provider = p;
22
+ cache.clear();
23
+ authTenantsCache.clear();
24
+ }
25
+ async function fetchShopifyConfig(siteName) {
26
+ const s = (key) => provider.get((0, extract_site_name_1.buildSecretId)(siteName, 'SHOPIFY', key));
27
+ const [adminApiAccessToken, isEnabledRaw, planName, clientId, clientSecret, storeDomain, storefrontApiVersion, storefrontPrivateToken, webhookHmacSecret,] = await Promise.all([
28
+ s('ADMIN_API_ACCESS_TOKEN'), s('IS_ENABLED'), s('PLAN_NAME'),
29
+ s('CLIENT_ID'), s('CLIENT_SECRET'), s('STORE_DOMAIN'),
30
+ s('STOREFRONT_API_VERSION'), s('STOREFRONT_PRIVATE_TOKEN'), s('WEBHOOK_HMAC_SECRET'),
31
+ ]);
32
+ return {
33
+ adminApiAccessToken,
34
+ isEnabled: isEnabledRaw.toLowerCase() === 'true',
35
+ planName, clientId, clientSecret, storeDomain,
36
+ storefrontApiVersion, storefrontPrivateToken, webhookHmacSecret,
37
+ };
38
+ }
39
+ /**
40
+ * realm/clientId/clientSecret and baseUrl are no longer read as their own
41
+ * `${SITENAME}_KEYCLOAK_*` secrets. Instead this mirrors how
42
+ * `@platform-x/hep-authentication-provider`'s `AuthenticationManager.iamAuthenticator`
43
+ * (see hep-delivery-service) resolves a tenant: realm/clientId/clientSecret
44
+ * come from the same AUTH_TENANTS_INFO entry it reads (via
45
+ * `getClientTenantDetails`), and baseUrl from the same `KEYCLOCK_HOST` env var
46
+ * the package itself reads. discoveryUrl/jwksUri/issuer follow Keycloak's
47
+ * fixed endpoint layout, derived from those two rather than stored — the same
48
+ * templates `generateKeycloakSecrets` uses to precompute them.
49
+ *
50
+ * callbackUrl/postLogoutRedirectUri stay as their own secrets, read under the
51
+ * `SHOPIFY` provider segment (`${SITENAME}_SHOPIFY_CALLBACK_URL` /
52
+ * `${SITENAME}_SHOPIFY_POST_LOGOUT_REDIRECT_URI`) rather than `KEYCLOAK` —
53
+ * they're Shopify-side external-IdP redirect URLs, not Keycloak realm data,
54
+ * so nothing about the tenant's realm can derive them.
55
+ */
56
+ async function fetchKeycloakConfig(siteName) {
57
+ const tenant = await getClientTenantDetails(siteName);
58
+ const clientInfo = tenant?.clients?.[0];
59
+ const realm = tenant?.tenant;
60
+ if (!realm || !clientInfo?.client_id || !clientInfo?.client_secret) {
61
+ throw new errors_1.SecretNotFoundError(`AUTH_TENANTS_INFO entry for ${siteName} is missing tenant/client data`);
62
+ }
63
+ const baseUrl = (process.env.KEYCLOCK_HOST ?? '').replace(/\/+$/, '');
64
+ if (!baseUrl) {
65
+ throw new errors_1.SecretNotFoundError('KEYCLOCK_HOST');
66
+ }
67
+ const [callbackUrl, postLogoutRedirectUri] = await Promise.all([
68
+ provider.get((0, extract_site_name_1.buildSecretId)(siteName, 'SHOPIFY', 'CALLBACK_URL')),
69
+ provider.get((0, extract_site_name_1.buildSecretId)(siteName, 'SHOPIFY', 'POST_LOGOUT_REDIRECT_URI')),
70
+ ]);
71
+ return {
72
+ baseUrl,
73
+ realm,
74
+ clientId: clientInfo.client_id,
75
+ clientSecret: clientInfo.client_secret,
76
+ discoveryUrl: `${baseUrl}/realms/${realm}/.well-known/openid-configuration`,
77
+ jwksUri: `${baseUrl}/realms/${realm}/protocol/openid-connect/certs`,
78
+ issuer: `${baseUrl}/realms/${realm}`,
79
+ callbackUrl,
80
+ postLogoutRedirectUri,
81
+ };
82
+ }
83
+ /**
84
+ * Main entry point. Fetches and caches tenant config for 5 minutes.
85
+ * @param siteName - Normalized site name e.g. 'KIWI'
86
+ */
87
+ async function getTenantConfig(siteName) {
88
+ const cached = cache.get(siteName);
89
+ if (cached)
90
+ return cached;
91
+ const [shopify, keycloak] = await Promise.all([
92
+ fetchShopifyConfig(siteName),
93
+ fetchKeycloakConfig(siteName),
94
+ ]);
95
+ const config = { siteName, shopify, keycloak };
96
+ cache.set(siteName, config);
97
+ return config;
98
+ }
99
+ /** Clear cached config for a tenant or all tenants. */
100
+ function clearTenantCache(siteName) {
101
+ if (siteName) {
102
+ cache.delete(siteName);
103
+ }
104
+ else {
105
+ cache.clear();
106
+ authTenantsCache.clear();
107
+ }
108
+ }
109
+ async function fetchAuthTenantsInfo() {
110
+ const cached = authTenantsCache.get(AUTH_TENANTS_INFO_SECRET_ID);
111
+ if (cached)
112
+ return cached;
113
+ const raw = await provider.get(AUTH_TENANTS_INFO_SECRET_ID);
114
+ // 'ascii' to match hep-delivery-service's base64ToString/stringToBase64,
115
+ // which is how this secret is encoded on write.
116
+ const tenants = JSON.parse(Buffer.from(raw, 'base64').toString('ascii'));
117
+ authTenantsCache.set(AUTH_TENANTS_INFO_SECRET_ID, tenants);
118
+ return tenants;
119
+ }
120
+ /**
121
+ * Looks up a site's entry in the AUTH_TENANTS_INFO secret (a base64-encoded
122
+ * JSON array). Returns `null` when the site isn't listed there, meaning it
123
+ * uses the shared realm rather than a dedicated one.
124
+ *
125
+ * Case-insensitive: `buildSecretId` already normalizes site names to
126
+ * uppercase for secret ids, so a caller passing e.g. 'kiwi' while
127
+ * AUTH_TENANTS_INFO stores 'KIWI' (or vice versa) must still match here —
128
+ * otherwise this silently returns null and the caller sees a confusing
129
+ * "missing tenant/client data" error instead of the real entry.
130
+ */
131
+ async function getClientTenantDetails(siteName) {
132
+ const tenants = await fetchAuthTenantsInfo();
133
+ const target = siteName.toUpperCase();
134
+ return tenants.find((tenant) => tenant.site_name.toUpperCase() === target) ?? null;
135
+ }
136
+ //# sourceMappingURL=tenant-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-config.js","sourceRoot":"","sources":["../src/tenant-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tls-trust.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tls-trust.d.ts","sourceRoot":"","sources":["../src/tls-trust.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_tls_1 = __importDefault(require("node:tls"));
7
+ /**
8
+ * Extends Node's default TLS trust store with the OS/system certificate store.
9
+ *
10
+ * `getGraphQLClient` calls Shopify through Node's built-in `fetch`, which only
11
+ * trusts a small bundled CA list by default — it does NOT include a corporate
12
+ * proxy's TLS-interception root CA. That CA IS already in the OS trust store
13
+ * (that's what lets browsers on the same machine reach HTTPS sites), so pulling
14
+ * it in here — once, for every consumer of this package — fixes
15
+ * "unable to get local issuer certificate" against Shopify/Keycloak without
16
+ * every service needing its own `--use-system-ca` flag or `cacert.pem` file.
17
+ *
18
+ * Runs once at module load (Node caches modules), process-wide. Falls back to
19
+ * the bundled default list on a Node/platform without OS store integration.
20
+ */
21
+ try {
22
+ node_tls_1.default.setDefaultCACertificates([...node_tls_1.default.getCACertificates('default'), ...node_tls_1.default.getCACertificates('system')]);
23
+ }
24
+ catch {
25
+ // No system store integration available — keep Node's bundled defaults.
26
+ }
27
+ //# sourceMappingURL=tls-trust.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tls-trust.js","sourceRoot":"","sources":["../src/tls-trust.ts"],"names":[],"mappings":";;;;;AAAA,wDAA2B;AAE3B;;;;;;;;;;;;;GAaG;AACH,IAAI,CAAC;IACH,kBAAG,CAAC,wBAAwB,CAAC,CAAC,GAAG,kBAAG,CAAC,iBAAiB,CAAC,SAAS,CAAC,EAAE,GAAG,kBAAG,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC1G,CAAC;AAAC,MAAM,CAAC;IACP,wEAAwE;AAC1E,CAAC"}
@@ -0,0 +1,40 @@
1
+ export interface ShopifyConfig {
2
+ adminApiAccessToken: string;
3
+ isEnabled: boolean;
4
+ planName: string;
5
+ clientId: string;
6
+ clientSecret: string;
7
+ storeDomain: string;
8
+ storefrontApiVersion: string;
9
+ storefrontPrivateToken: string;
10
+ webhookHmacSecret: string;
11
+ }
12
+ export interface KeycloakConfig {
13
+ baseUrl: string;
14
+ realm: string;
15
+ clientId: string;
16
+ clientSecret: string;
17
+ discoveryUrl: string;
18
+ jwksUri: string;
19
+ issuer: string;
20
+ callbackUrl: string;
21
+ postLogoutRedirectUri: string;
22
+ }
23
+ export interface TenantConfig {
24
+ siteName: string;
25
+ shopify: ShopifyConfig;
26
+ keycloak: KeycloakConfig;
27
+ }
28
+ export interface ConfigProvider {
29
+ get(secretId: string): Promise<string>;
30
+ }
31
+ /**
32
+ * One entry from the AUTH_TENANTS_INFO secret — a base64-encoded JSON array
33
+ * listing every site that has a dedicated Keycloak realm. A site absent from
34
+ * this list uses the shared realm instead.
35
+ */
36
+ export interface TenantAuthInfo {
37
+ site_name: string;
38
+ [key: string]: unknown;
39
+ }
40
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACxC;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@platform-x/shp-auth-client",
3
+ "version": "0.0.1",
4
+ "description": "Shared Shopify/Keycloak tenant config resolver, resilient GraphQL client, and Keycloak token verification for Platform-X microservices",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "license": "UNLICENSED",
11
+ "author": "HCL-X",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/hcl-x/shp-auth-client.git"
15
+ },
16
+ "homepage": "https://github.com/hcl-x/shp-auth-client#readme",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "node ./node_modules/typescript/bin/tsc -p tsconfig.build.json",
22
+ "dev": "node ./node_modules/typescript/bin/tsc -p tsconfig.build.json --watch",
23
+ "clean": "rm -rf dist",
24
+ "lint": "eslint .",
25
+ "format": "prettier --write .",
26
+ "test": "cross-env NODE_ENV=test jest --runInBand",
27
+ "test:watch": "jest --watch",
28
+ "typecheck": "node ./node_modules/typescript/bin/tsc --noEmit"
29
+ },
30
+ "overrides": {
31
+ "axios": "^1.20.0",
32
+ "qs": "^6.16.0",
33
+ "uuid": "11.1.1",
34
+ "@platform-x/hep-authentication-provider": {
35
+ "uuid": "11.1.1"
36
+ }
37
+ },
38
+ "dependencies": {
39
+ "@platform-x/hep-authentication-provider": "2.3.24",
40
+ "graphql": "^16.8.0",
41
+ "graphql-request": "^6.1.0",
42
+ "hep-secret-access": "^1.3.4",
43
+ "jsonwebtoken": "^9.0.3",
44
+ "jwks-rsa": "^3.2.2"
45
+ },
46
+ "peerDependencies": {
47
+ "express": "^4.18.0"
48
+ },
49
+ "devDependencies": {
50
+ "@eslint/js": "^9.18.0",
51
+ "@types/express": "^4.17.21",
52
+ "@types/jest": "^29.5.14",
53
+ "@types/jsonwebtoken": "^9.0.10",
54
+ "@types/node": "^22.10.0",
55
+ "@typescript-eslint/eslint-plugin": "^8.18.0",
56
+ "@typescript-eslint/parser": "^8.18.0",
57
+ "cross-env": "^7.0.3",
58
+ "eslint": "^9.18.0",
59
+ "eslint-config-prettier": "^9.1.0",
60
+ "eslint-import-resolver-typescript": "^3.10.1",
61
+ "eslint-plugin-import": "^2.31.0",
62
+ "globals": "^14.0.0",
63
+ "jest": "^29.7.0",
64
+ "prettier": "^3.4.2",
65
+ "ts-jest": "^29.2.5",
66
+ "ts-node": "^10.9.2",
67
+ "typescript": "^5.3.0"
68
+ }
69
+ }