@ductape/sdk 0.1.154 → 0.1.156

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/dist/index.js CHANGED
@@ -66,8 +66,10 @@ const runtime_defaults_1 = require("./runtime/runtime-defaults");
66
66
  const healthcheck_cache_key_1 = require("./resilience/healthcheck-cache-key");
67
67
  const enums_1 = require("./types/enums");
68
68
  const productsApi_service_1 = require("./api/services/productsApi.service");
69
+ const bootstrap_cache_1 = require("./products/bootstrap-cache");
69
70
  const imports_service_1 = __importDefault(require("./imports/imports.service"));
70
71
  const processor_service_1 = __importDefault(require("./processor/services/processor.service"));
72
+ const processor_utils_1 = require("./processor/utils/processor.utils");
71
73
  const logs_service_1 = __importDefault(require("./logs/logs.service"));
72
74
  const mime = __importStar(require("mime-types"));
73
75
  const database_1 = require("./database");
@@ -500,7 +502,8 @@ class Ductape {
500
502
  console.warn(`[Ductape] Redis unavailable (${reason}); using in-memory bootstrap cache.`);
501
503
  }
502
504
  }
503
- constructor({ accessKey, redis_url, product, env, local, ip_address, language }) {
505
+ constructor({ accessKey, redis_url, product, env, local, ip_address, language, runtime_sync }) {
506
+ var _a, _b, _c;
504
507
  this.productBuilders = new Map();
505
508
  this.appBuilders = new Map();
506
509
  // Cached service instances (lazily initialized after auth)
@@ -523,6 +526,11 @@ class Ductape {
523
526
  this._brokersService = null;
524
527
  /** Prevent every lazy service factory from retrying a Redis URL that already failed. */
525
528
  this.redisCacheUnavailable = false;
529
+ this.runtimeSyncEnabled = true;
530
+ this.runtimePollIntervalMs = 30000;
531
+ this.runtimePollJitter = 0.2;
532
+ this.runtimeMaxBackoffMs = 300000;
533
+ this.runtimeClosed = false;
526
534
  /** Register and inspect portable application functions used by Feature JSON. */
527
535
  this.functions = {
528
536
  register: (contract) => functions_1.portableFunctions.register(contract),
@@ -5440,8 +5448,20 @@ class Ductape {
5440
5448
  this.workspaceApi = new workspaceApi_service_1.WorkspaceApiService(this.environment);
5441
5449
  this.productsApi = new productsApi_service_1.ProductsApiService(this.environment);
5442
5450
  this.redis_url = redis_url;
5451
+ this.runtimeSyncEnabled = runtime_sync !== false && Boolean(productTag && envSlug);
5452
+ if (runtime_sync && typeof runtime_sync === 'object') {
5453
+ this.runtimePollIntervalMs = Math.max(5000, (_a = runtime_sync.interval_ms) !== null && _a !== void 0 ? _a : 30000);
5454
+ this.runtimePollJitter = Math.max(0, Math.min(1, (_b = runtime_sync.jitter) !== null && _b !== void 0 ? _b : 0.2));
5455
+ this.runtimeMaxBackoffMs = Math.max(this.runtimePollIntervalMs, (_c = runtime_sync.max_backoff_ms) !== null && _c !== void 0 ? _c : 300000);
5456
+ }
5443
5457
  // Services are lazily initialized via factory methods (createNewDatabaseService, etc.)
5444
5458
  // This ensures initUserAuth() is called before service creation
5459
+ if (this.runtimeSyncEnabled) {
5460
+ this.authInFlight = this.performUserAuth(false).finally(() => { this.authInFlight = undefined; });
5461
+ this.authInFlight.catch((error) => {
5462
+ this.runtimeLastError = error instanceof Error ? error.message : String(error);
5463
+ });
5464
+ }
5445
5465
  return this; //createDeepProxy(this, this.environment) as this;
5446
5466
  }
5447
5467
  async loadBullMQ() {
@@ -5709,9 +5729,17 @@ class Ductape {
5709
5729
  return this.api;
5710
5730
  }
5711
5731
  async initUserAuth(subCheck = true) {
5732
+ if (this.authInFlight)
5733
+ return await this.authInFlight;
5734
+ this.authInFlight = this.performUserAuth(subCheck).finally(() => { this.authInFlight = undefined; });
5735
+ return await this.authInFlight;
5736
+ }
5737
+ async performUserAuth(subCheck = true) {
5712
5738
  var _a, _b;
5713
5739
  // Skip if already authenticated with valid credentials
5714
5740
  if (this.token && this.public_key) {
5741
+ if (this.runtimeSyncEnabled && !this.runtimeManifest)
5742
+ await this.refreshRuntimeSnapshot(true);
5715
5743
  return;
5716
5744
  }
5717
5745
  // Only reset if we need to re-authenticate
@@ -5742,6 +5770,15 @@ class Ductape {
5742
5770
  // Don't throw - private key is only needed for secrets encryption
5743
5771
  }
5744
5772
  }
5773
+ if (this.runtimeSyncEnabled && !this.runtimeClosed) {
5774
+ try {
5775
+ await this.refreshRuntimeSnapshot(true);
5776
+ }
5777
+ catch (error) {
5778
+ this.runtimeLastError = error instanceof Error ? error.message : String(error);
5779
+ this.scheduleRuntimePoll(1);
5780
+ }
5781
+ }
5745
5782
  }
5746
5783
  catch (e) {
5747
5784
  const error = ((_b = (_a = e.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.errors) || e.message || 'Unknown error';
@@ -5749,6 +5786,161 @@ class Ductape {
5749
5786
  throw new Error(error);
5750
5787
  }
5751
5788
  }
5789
+ runtimeAuth() {
5790
+ return {
5791
+ token: this.token,
5792
+ public_key: this.public_key,
5793
+ user_id: this.user_id,
5794
+ workspace_id: this.workspace_id,
5795
+ access_key: this.accessKey,
5796
+ };
5797
+ }
5798
+ applyRuntimeSnapshot(response) {
5799
+ var _a, _b, _c, _d, _e, _f, _g;
5800
+ if (!(response === null || response === void 0 ? void 0 : response.snapshot))
5801
+ return;
5802
+ const scope = `${this.workspace_id}:${response.snapshot.product_tag}:${response.snapshot.env_slug}`;
5803
+ (0, bootstrap_cache_1.primeRuntimeBootstrapCache)(this.workspace_id, scope, response.snapshot.entries);
5804
+ const runtimeCredentials = new Map();
5805
+ for (const entry of response.snapshot.entries) {
5806
+ if ((entry === null || entry === void 0 ? void 0 : entry.domain) !== 'action')
5807
+ continue;
5808
+ const app = String(((_a = entry.parts) === null || _a === void 0 ? void 0 : _a[1]) || '');
5809
+ const mapping = (_b = entry.data) === null || _b === void 0 ? void 0 : _b.product_env_mapping;
5810
+ if (!app || !mapping)
5811
+ continue;
5812
+ const credentials = (_c = runtimeCredentials.get(app)) !== null && _c !== void 0 ? _c : {};
5813
+ for (const variable of (_d = mapping.variables) !== null && _d !== void 0 ? _d : []) {
5814
+ if ((variable === null || variable === void 0 ? void 0 : variable.key) && variable.value !== undefined)
5815
+ credentials[String(variable.key)] = variable.value;
5816
+ }
5817
+ // token_access data is the reusable request credential itself. credential_access
5818
+ // data contains login secrets and must only be sent to its dedicated auth action.
5819
+ if (((_f = (_e = entry.data) === null || _e === void 0 ? void 0 : _e.app_auth) === null || _f === void 0 ? void 0 : _f.setup_type) === 'token_access' && ((_g = mapping.auth) === null || _g === void 0 ? void 0 : _g.data)) {
5820
+ let authData = mapping.auth.data;
5821
+ if (typeof authData === 'string') {
5822
+ try {
5823
+ authData = JSON.parse((0, processor_utils_1.decrypt)(authData, String(entry.data.private_key)));
5824
+ }
5825
+ catch (_h) {
5826
+ authData = undefined;
5827
+ }
5828
+ }
5829
+ if (authData && typeof authData === 'object') {
5830
+ for (const location of ['headers', 'query', 'params', 'body']) {
5831
+ const values = authData[location];
5832
+ if (!values || typeof values !== 'object' || Array.isArray(values))
5833
+ continue;
5834
+ for (const [key, value] of Object.entries(values)) {
5835
+ credentials[`${location}:${key}`] = value;
5836
+ }
5837
+ }
5838
+ }
5839
+ }
5840
+ runtimeCredentials.set(app, credentials);
5841
+ }
5842
+ credential_manager_1.credentialManager.replaceRuntimeScope(response.snapshot.product_tag, response.snapshot.env_slug, [...runtimeCredentials].map(([app, credentials]) => ({ app, credentials })));
5843
+ this.runtimeManifest = response.manifest;
5844
+ this.runtimeLastRefreshAt = Date.now();
5845
+ this.runtimeLastError = undefined;
5846
+ }
5847
+ scheduleRuntimePoll(failureCount = 0) {
5848
+ var _a, _b;
5849
+ if (!this.runtimeSyncEnabled || this.runtimeClosed)
5850
+ return;
5851
+ if (this.runtimePollTimer)
5852
+ clearTimeout(this.runtimePollTimer);
5853
+ const exponential = Math.min(this.runtimeMaxBackoffMs, this.runtimePollIntervalMs * Math.pow(2, failureCount));
5854
+ const jitter = exponential * this.runtimePollJitter * ((Math.random() * 2) - 1);
5855
+ this.runtimePollTimer = setTimeout(() => {
5856
+ this.pollRuntimeManifest(failureCount).catch(() => undefined);
5857
+ }, Math.max(1000, Math.round(exponential + jitter)));
5858
+ (_b = (_a = this.runtimePollTimer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
5859
+ }
5860
+ async pollRuntimeManifest(failureCount) {
5861
+ var _a, _b;
5862
+ if (!this.defaultProduct || !this.defaultEnv || this.runtimeClosed)
5863
+ return;
5864
+ try {
5865
+ const manifestResponse = await this.productsApi.bootstrapRuntime({
5866
+ product_tag: this.defaultProduct,
5867
+ env_slug: this.defaultEnv,
5868
+ manifest_only: true,
5869
+ etag: (_a = this.runtimeManifest) === null || _a === void 0 ? void 0 : _a.runtimeRevision,
5870
+ }, this.runtimeAuth());
5871
+ if (manifestResponse && manifestResponse.manifest.runtimeRevision !== ((_b = this.runtimeManifest) === null || _b === void 0 ? void 0 : _b.runtimeRevision)) {
5872
+ await this.refreshRuntimeSnapshot(true);
5873
+ }
5874
+ else if (this.defaultProduct && this.defaultEnv) {
5875
+ (0, bootstrap_cache_1.touchRuntimeBootstrapCache)(`${this.workspace_id}:${this.defaultProduct}:${this.defaultEnv}`);
5876
+ }
5877
+ this.runtimeLastError = undefined;
5878
+ this.scheduleRuntimePoll(0);
5879
+ }
5880
+ catch (error) {
5881
+ this.runtimeLastError = error instanceof Error ? error.message : String(error);
5882
+ this.scheduleRuntimePoll(Math.min(failureCount + 1, 8));
5883
+ }
5884
+ }
5885
+ async refreshRuntimeSnapshot(force = false) {
5886
+ if (!this.runtimeSyncEnabled || !this.defaultProduct || !this.defaultEnv || this.runtimeClosed)
5887
+ return;
5888
+ if (this.runtimeRefreshInFlight)
5889
+ return await this.runtimeRefreshInFlight;
5890
+ this.runtimeRefreshInFlight = (async () => {
5891
+ var _a;
5892
+ const response = await this.productsApi.bootstrapRuntime(Object.assign({ product_tag: this.defaultProduct, env_slug: this.defaultEnv }, (force ? {} : { etag: (_a = this.runtimeManifest) === null || _a === void 0 ? void 0 : _a.runtimeRevision })), this.runtimeAuth());
5893
+ if (response)
5894
+ this.applyRuntimeSnapshot(response);
5895
+ this.scheduleRuntimePoll(0);
5896
+ })().finally(() => { this.runtimeRefreshInFlight = undefined; });
5897
+ return await this.runtimeRefreshInFlight;
5898
+ }
5899
+ /** Force a full product runtime refresh and atomically replace bootstrap cache entries. */
5900
+ async ready() {
5901
+ await this.initUserAuth(false);
5902
+ }
5903
+ /** Force a full product runtime refresh and atomically replace bootstrap cache entries. */
5904
+ async refreshRuntime() {
5905
+ await this.initUserAuth(false);
5906
+ await this.refreshRuntimeSnapshot(true);
5907
+ }
5908
+ /** Observe the local product runtime snapshot without exposing cached configuration or secrets. */
5909
+ runtimeSnapshotStatus() {
5910
+ return {
5911
+ enabled: this.runtimeSyncEnabled,
5912
+ product: this.defaultProduct,
5913
+ env: this.defaultEnv,
5914
+ manifest: this.runtimeManifest ? Object.assign({}, this.runtimeManifest) : undefined,
5915
+ lastRefreshAt: this.runtimeLastRefreshAt,
5916
+ lastError: this.runtimeLastError,
5917
+ refreshing: Boolean(this.runtimeRefreshInFlight),
5918
+ };
5919
+ }
5920
+ /** Stop background synchronization and release SDK-owned background resources. */
5921
+ async close() {
5922
+ var _a;
5923
+ this.runtimeClosed = true;
5924
+ if (this.runtimePollTimer)
5925
+ clearTimeout(this.runtimePollTimer);
5926
+ this.runtimePollTimer = undefined;
5927
+ await ((_a = this.runtimeRefreshInFlight) === null || _a === void 0 ? void 0 : _a.catch(() => undefined));
5928
+ const resources = [this.jobsWorker, this.healthCheckWorker, this.healthCheckUpdaterWorker,
5929
+ this.jobsQueue, this.healthCheckQueue, this.healthCheckUpdaterQueue];
5930
+ await Promise.all(resources.filter(Boolean).map((resource) => { var _a; return Promise.resolve((_a = resource.close) === null || _a === void 0 ? void 0 : _a.call(resource)).catch(() => undefined); }));
5931
+ if (this.redisClient) {
5932
+ try {
5933
+ await this.redisClient.quit();
5934
+ }
5935
+ catch (_b) {
5936
+ try {
5937
+ this.redisClient.disconnect(false);
5938
+ }
5939
+ catch ( /* best effort */_c) { /* best effort */ }
5940
+ }
5941
+ this.redisClient = undefined;
5942
+ }
5943
+ }
5752
5944
  createNewLogger(product_id, app_id) {
5753
5945
  return new logs_service_1.default(Object.assign(Object.assign({}, this.builderInit()), { product_id,
5754
5946
  app_id }));