@dx-do/client 6.3.5 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs.js CHANGED
@@ -2036,7 +2036,7 @@ utils$3.reduceDescriptors(AxiosHeaders$1.prototype, ({ value }, key) => {
2036
2036
 
2037
2037
  utils$3.freezeMethods(AxiosHeaders$1);
2038
2038
 
2039
- const REDACTED = '[REDACTED ****]';
2039
+ const REDACTED$1 = '[REDACTED ****]';
2040
2040
 
2041
2041
  function hasOwnOrPrototypeToJSON(source) {
2042
2042
  if (utils$3.hasOwnProp(source, 'toJSON')) {
@@ -2091,7 +2091,7 @@ function redactConfig(config, redactKeys) {
2091
2091
 
2092
2092
  result = Object.create(null);
2093
2093
  for (const [key, value] of Object.entries(source)) {
2094
- const reducedValue = lowerKeys.has(key.toLowerCase()) ? REDACTED : visit(value);
2094
+ const reducedValue = lowerKeys.has(key.toLowerCase()) ? REDACTED$1 : visit(value);
2095
2095
  if (!utils$3.isUndefined(reducedValue)) {
2096
2096
  result[key] = reducedValue;
2097
2097
  }
@@ -14712,6 +14712,18 @@ var setToStringTag = esSetTostringtag;
14712
14712
  var hasOwn$1 = hasown;
14713
14713
  var populate = populate$1;
14714
14714
 
14715
+ /**
14716
+ * Escape CR, LF, and `"` in a multipart `name`/`filename` parameter, so a field
14717
+ * name or filename can not break out of its header line to inject headers or
14718
+ * smuggle additional parts. Matches the WHATWG HTML multipart/form-data encoding.
14719
+ *
14720
+ * @param {string} str - the parameter value to escape
14721
+ * @returns {string} the escaped value
14722
+ */
14723
+ function escapeHeaderParam(str) {
14724
+ return String(str).replace(/\r/g, '%0D').replace(/\n/g, '%0A').replace(/"/g, '%22');
14725
+ }
14726
+
14715
14727
  /**
14716
14728
  * Create readable "multipart/form-data" streams.
14717
14729
  * Can be used to submit forms
@@ -14877,7 +14889,7 @@ FormData$1.prototype._multiPartHeader = function (field, value, options) {
14877
14889
  var contents = '';
14878
14890
  var headers = {
14879
14891
  // add custom disposition as third element or keep it two elements if not
14880
- 'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []),
14892
+ 'Content-Disposition': ['form-data', 'name="' + escapeHeaderParam(field) + '"'].concat(contentDisposition || []),
14881
14893
  // if no content type. allow it to be empty array
14882
14894
  'Content-Type': [].concat(contentType || [])
14883
14895
  };
@@ -14931,7 +14943,7 @@ FormData$1.prototype._getContentDisposition = function (value, options) { // esl
14931
14943
  }
14932
14944
 
14933
14945
  if (filename) {
14934
- return 'filename="' + filename + '"';
14946
+ return 'filename="' + escapeHeaderParam(filename) + '"';
14935
14947
  }
14936
14948
  };
14937
14949
 
@@ -61955,6 +61967,99 @@ class StrictMap extends Map {
61955
61967
  }
61956
61968
  }
61957
61969
 
61970
+ /**
61971
+ * Redaction helpers for keeping auth tokens and other secrets out of logs.
61972
+ *
61973
+ * Auth tokens live in axios `error.config.headers` — e.g. `Authorization: Bearer
61974
+ * <userToken>` and `X-Asm-Auth: <dxASMToken>`. Anything that serializes a whole
61975
+ * error / config / headers object (`JSON.stringify`, an AxiosError's `.toJSON()`,
61976
+ * `console.log(err)`, or a logger that deep-prints its argument) will spill them.
61977
+ * These helpers strip the secrets before anything is logged or re-thrown.
61978
+ */
61979
+ const REDACTED = '[REDACTED]';
61980
+ /** Lowercased object-key names whose values must never be logged. */
61981
+ const SENSITIVE_KEYS = new Set([
61982
+ 'authorization',
61983
+ 'x-asm-auth',
61984
+ 'cookie',
61985
+ 'set-cookie',
61986
+ 'proxy-authorization',
61987
+ 'usertoken',
61988
+ 'dxasmtoken',
61989
+ 'tenanttoken',
61990
+ 'agenttoken',
61991
+ 'accesstoken',
61992
+ 'refreshtoken',
61993
+ 'password',
61994
+ 'apikey',
61995
+ 'x-api-key',
61996
+ ]);
61997
+ /** Matches `Bearer <token>` anywhere in a string. */
61998
+ const BEARER = /Bearer\s+\S+/gi;
61999
+ function redactString(value) {
62000
+ return value.replace(BEARER, `Bearer ${REDACTED}`);
62001
+ }
62002
+ /**
62003
+ * Returns a deep copy of `value` with sensitive object keys masked and any
62004
+ * `Bearer <token>` substrings scrubbed. Safe to pass anything: it is
62005
+ * circular-reference safe (axios errors are circular), and `Error` instances are
62006
+ * reduced to `{ name, message, stack }` (all scrubbed) so their token-bearing
62007
+ * `config`/`request`/`response` are dropped entirely.
62008
+ */
62009
+ function redactValue(value, seen = new WeakSet()) {
62010
+ if (typeof value === 'string')
62011
+ return redactString(value);
62012
+ if (value === null || typeof value !== 'object')
62013
+ return value;
62014
+ if (seen.has(value))
62015
+ return '[Circular]';
62016
+ seen.add(value);
62017
+ if (Array.isArray(value)) {
62018
+ return value.map((entry) => redactValue(entry, seen));
62019
+ }
62020
+ if (value instanceof Error) {
62021
+ return {
62022
+ name: value.name,
62023
+ message: redactString(value.message),
62024
+ stack: value.stack ? redactString(value.stack) : undefined,
62025
+ };
62026
+ }
62027
+ const out = {};
62028
+ for (const [key, entry] of Object.entries(value)) {
62029
+ out[key] = SENSITIVE_KEYS.has(key.toLowerCase())
62030
+ ? REDACTED
62031
+ : redactValue(entry, seen);
62032
+ }
62033
+ return out;
62034
+ }
62035
+ /**
62036
+ * Strips auth tokens from an (possibly Axios) error IN PLACE and returns it, so it
62037
+ * stays a throwable `Error` with its stack intact but no longer carries the token
62038
+ * in `config.headers`, `response.config.headers`, or the raw request header string.
62039
+ * Non-error / non-axios values are returned unchanged.
62040
+ *
62041
+ * Use this at the axios boundary (a response error interceptor) so every catch,
62042
+ * re-throw, and top-level handler downstream sees a token-free error.
62043
+ */
62044
+ function sanitizeAxiosError(err) {
62045
+ if (!err || typeof err !== 'object')
62046
+ return err;
62047
+ const e = err;
62048
+ // Only touch things that look like axios errors.
62049
+ if (!e.isAxiosError && !e.config && !e.response)
62050
+ return err;
62051
+ if (e.config?.headers)
62052
+ e.config.headers = redactValue(e.config.headers);
62053
+ if (e.response?.config?.headers) {
62054
+ e.response.config.headers = redactValue(e.response.config.headers);
62055
+ }
62056
+ // Node's ClientRequest keeps the raw request (incl. the Authorization line) in `_header`.
62057
+ if (e.request && typeof e.request._header === 'string') {
62058
+ e.request._header = redactString(e.request._header);
62059
+ }
62060
+ return err;
62061
+ }
62062
+
61958
62063
  /**
61959
62064
  * Service for APM agents: query by name/regex, list names/statuses, traces,
61960
62065
  * thread dumps, and license/device reporting.
@@ -64597,12 +64702,15 @@ class AsmService {
64597
64702
  baseURL =
64598
64703
  this.dxSaaSService.dxdoConfiguration.dxConfiguration.dxASMbaseAPIURL;
64599
64704
  }
64600
- this.localClient = createClient({
64705
+ const client = createClient({
64601
64706
  baseURL,
64602
64707
  headers: {
64603
64708
  'X-Asm-Auth': this.dxSaaSService.dxdoConfiguration.dxConfiguration.dxASMToken,
64604
64709
  },
64605
64710
  });
64711
+ // Strip the X-Asm-Auth token from any error before it can be logged/propagated.
64712
+ client.instance.interceptors.response.use((response) => response, (error) => Promise.reject(sanitizeAxiosError(error)));
64713
+ this.localClient = client;
64606
64714
  }
64607
64715
  return this.localClient;
64608
64716
  }
@@ -64615,7 +64723,7 @@ class AsmService {
64615
64723
  return foldersList.data;
64616
64724
  })
64617
64725
  .catch((error) => {
64618
- this.log.error(error);
64726
+ this.log.error('ASM foldersList failed', error instanceof Error ? error.message : String(error));
64619
64727
  throw error;
64620
64728
  });
64621
64729
  }
@@ -64723,6 +64831,23 @@ class AuthorizationService {
64723
64831
  }
64724
64832
  return raw;
64725
64833
  }
64834
+ /**
64835
+ * Returns true if a user with the given identifier exists on the tenant.
64836
+ * Matches case-insensitively against each account's `userId` (the login
64837
+ * principal — usually the upper-cased email) and any of its `emailIds`.
64838
+ *
64839
+ * @remarks Fetches the user list via {@link getUsersV2} (a single page, up
64840
+ * to 5000 accounts). A tenant with more users than that could yield a false
64841
+ * negative for an account beyond the page.
64842
+ */
64843
+ async userExists(userIdentifier) {
64844
+ const needle = userIdentifier.trim().toUpperCase();
64845
+ if (needle.length === 0)
64846
+ return false;
64847
+ const response = await this.getUsersV2(1, 5000);
64848
+ return response.users.some((user) => (user.userId ?? '').toUpperCase() === needle ||
64849
+ (user.emailIds ?? []).some((email) => (email.emailAddr ?? '').toUpperCase() === needle));
64850
+ }
64726
64851
  /** Reformats a cohort id to upper-cased, dashed GUID form (`8-4-4-4-12`). */
64727
64852
  toGuidCohortId(cohortId) {
64728
64853
  const hex = cohortId.replace(/-/g, '').toLowerCase();
@@ -70251,6 +70376,9 @@ class DxSaasService {
70251
70376
  .replace(/-/g, '')
70252
70377
  .toLowerCase();
70253
70378
  this.initializeProxyIfNecessary();
70379
+ // Strip auth tokens from any error this client surfaces, so nothing downstream
70380
+ // (handleAxiosError, re-throws, top-level catches) can log the Authorization header.
70381
+ this._httpClient.interceptors.response.use((response) => response, (error) => Promise.reject(sanitizeAxiosError(error)));
70254
70382
  if (process__namespace.env.DXDO_LOG_REQUESTS) {
70255
70383
  log.info('Logging requests');
70256
70384
  const requestLogFile = process__namespace.env.DXDO_REQUEST_LOG_FILE ??
@@ -71210,20 +71338,21 @@ class DxSaasService {
71210
71338
  }
71211
71339
  static handleAxiosError(reason, isStream = false) {
71212
71340
  if (reason.isAxiosError && reason.response) {
71213
- DxSaasService.staticLog.debug('[Axios Error DEBUG]', reason.toJSON());
71214
- DxSaasService.staticLog.error('[Axios Error BASIC]', `${reason.request.method} ${reason.config?.url} ${JSON.stringify(reason.config?.params ?? {})}`);
71215
- DxSaasService.staticLog.error('[Axios Error HEADER]', `${JSON.stringify(reason.config?.headers)}`);
71216
- DxSaasService.staticLog.error('[Axios Error BODY]', reason.config?.data ?? {});
71341
+ // NB: request headers are already token-scrubbed by the response interceptor;
71342
+ // redactValue is belt-and-suspenders in case this is ever called off-path.
71343
+ DxSaasService.staticLog.debug('[Axios Error DEBUG]', redactValue(reason.toJSON()));
71344
+ DxSaasService.staticLog.error('[Axios Error BASIC]', `${reason.request.method} ${reason.config?.url} ${JSON.stringify(redactValue(reason.config?.params ?? {}))}`);
71345
+ DxSaasService.staticLog.error('[Axios Error BODY]', redactValue(reason.config?.data ?? {}));
71217
71346
  if (isStream) {
71218
71347
  DxSaasService.staticLog.error(`[Axios Error STATUS] ${reason.response.status} / ${reason.message} `);
71219
71348
  }
71220
71349
  else {
71221
- DxSaasService.staticLog.error(`[Axios Error STATUS] ${reason.response.status} / ${reason.message} / ${JSON.stringify(reason.response.data)}`);
71350
+ DxSaasService.staticLog.error(`[Axios Error STATUS] ${reason.response.status} / ${reason.message} / ${JSON.stringify(redactValue(reason.response.data))}`);
71222
71351
  }
71223
71352
  return new SimpleHTTPError(reason.response.status, reason.message);
71224
71353
  }
71225
71354
  else
71226
- return reason; // stupid.
71355
+ return sanitizeAxiosError(reason); // scrub before propagating
71227
71356
  }
71228
71357
  static async legacyAuth(v3Configuration) {
71229
71358
  return this.legacyAxaRequest(v3Configuration, 'GET', 'ess/security/v1/me', false, false);
@@ -78728,6 +78857,419 @@ class DataStoreNASSService {
78728
78857
  }
78729
78858
  }
78730
78859
 
78860
+ /**
78861
+ * Service Universe models — an **OI-owned universe (view) scoped to a curated
78862
+ * list of services**, managed through the tenant `views/*` API. The wire shape
78863
+ * here matches the real server payload (verified against a live tenant): `views`
78864
+ * is an **array** of typed sub-views and access grants carry a `permissions`
78865
+ * array. (This is deliberately distinct from the object-shaped `ViewBodySchema`
78866
+ * in `./query`, which models a different, unused surface.)
78867
+ */
78868
+ /** Access permission granted on a service universe. */
78869
+ const ServiceUniversePermissionSchema = v4.z
78870
+ .enum(['READ', 'WRITE'])
78871
+ .describe('Permission granted on the universe');
78872
+ /** A single user or group access grant on a service universe. */
78873
+ const ServiceUniverseAccessEntrySchema = v4.z.looseObject({
78874
+ name: v4.z.string().describe('User email or group name being granted access'),
78875
+ permissions: v4.z
78876
+ .array(ServiceUniversePermissionSchema)
78877
+ .optional()
78878
+ .describe('Permissions granted to this principal (READ and/or WRITE)'),
78879
+ canManage: v4.z
78880
+ .boolean()
78881
+ .optional()
78882
+ .describe('Whether the principal may manage (re-share) the universe'),
78883
+ attributeInclude: v4.z
78884
+ .array(v4.z.unknown())
78885
+ .optional()
78886
+ .describe('Attribute-based include scoping for this grant'),
78887
+ attributeExclude: v4.z
78888
+ .array(v4.z.unknown())
78889
+ .optional()
78890
+ .describe('Attribute-based exclude scoping for this grant'),
78891
+ isValid: v4.z
78892
+ .array(v4.z.string())
78893
+ .optional()
78894
+ .describe('Server-side validation hints for the principal (UI-supplied)'),
78895
+ });
78896
+ /** Access policy for a service universe (per-user and per-group grants). */
78897
+ const ServiceUniverseAccessSchema = v4.z.looseObject({
78898
+ users: v4.z
78899
+ .array(ServiceUniverseAccessEntrySchema)
78900
+ .optional()
78901
+ .describe('Per-user access grants'),
78902
+ groups: v4.z
78903
+ .array(ServiceUniverseAccessEntrySchema)
78904
+ .optional()
78905
+ .describe('Per-group access grants'),
78906
+ });
78907
+ /** The TAS scoping of a service universe — its inner filter selects the services. */
78908
+ const ServiceUniverseTasSpecSchema = v4.z.looseObject({
78909
+ filter: TasFilterSchema.optional().describe('TAS filter selecting the topology; for a service universe this is a SERVICE-op filter over the chosen service names'),
78910
+ includeMetricFilter: v4.z
78911
+ .boolean()
78912
+ .optional()
78913
+ .describe('Whether a metric filter is applied; defaults to false'),
78914
+ projection: v4.z
78915
+ .string()
78916
+ .optional()
78917
+ .describe("Projection mode, e.g. 'NO_TYPE'"),
78918
+ projectionFilter: v4.z
78919
+ .looseObject({
78920
+ attributes: v4.z
78921
+ .array(v4.z.string())
78922
+ .optional()
78923
+ .describe('Attributes to project, e.g. ["name"]'),
78924
+ })
78925
+ .optional()
78926
+ .describe('Which vertex attributes to project'),
78927
+ });
78928
+ /** A TAS sub-view of a service universe. */
78929
+ const ServiceUniverseTasViewSchema = v4.z.looseObject({
78930
+ type: v4.z.literal('tas').describe('Discriminator: topology (TAS) sub-view'),
78931
+ filter: ServiceUniverseTasSpecSchema.describe('TAS scoping for this sub-view'),
78932
+ permissions: v4.z
78933
+ .array(ServiceUniversePermissionSchema)
78934
+ .optional()
78935
+ .describe('Permissions on this sub-view'),
78936
+ });
78937
+ /** A NASS (metrics) sub-view of a service universe. */
78938
+ const ServiceUniverseNassViewSchema = v4.z.looseObject({
78939
+ type: v4.z.literal('nass').describe('Discriminator: metrics (NASS) sub-view'),
78940
+ filter: v4.z
78941
+ .looseObject({
78942
+ filter: v4.z
78943
+ .looseObject({ op: v4.z.string().describe('Filter op, e.g. "ALL"') })
78944
+ .optional()
78945
+ .describe('NASS filter tree'),
78946
+ serviceFilter: v4.z
78947
+ .unknown()
78948
+ .optional()
78949
+ .describe('Optional service-scoped NASS filter'),
78950
+ })
78951
+ .optional()
78952
+ .describe('NASS scoping for this sub-view'),
78953
+ permissions: v4.z
78954
+ .array(ServiceUniversePermissionSchema)
78955
+ .optional()
78956
+ .describe('Permissions on this sub-view'),
78957
+ });
78958
+ /** A service universe sub-view: TAS (service-scoped topology) or NASS (metrics). */
78959
+ const ServiceUniverseViewSchema = v4.z
78960
+ .discriminatedUnion('type', [
78961
+ ServiceUniverseTasViewSchema,
78962
+ ServiceUniverseNassViewSchema,
78963
+ ])
78964
+ .describe('A TAS or NASS sub-view of the universe');
78965
+ /** Metadata for a service universe. */
78966
+ const ServiceUniverseAttributesSchema = v4.z.looseObject({
78967
+ owner: v4.z
78968
+ .string()
78969
+ .optional()
78970
+ .describe("Owning product; service universes are 'OI'"),
78971
+ label: v4.z.string().describe('Human-readable universe name'),
78972
+ description: v4.z.string().optional().describe('Universe description'),
78973
+ });
78974
+ /**
78975
+ * The create/update request body for a service universe
78976
+ * (`POST/PUT views/view`).
78977
+ */
78978
+ const ServiceUniverseBodySchema = v4.z.looseObject({
78979
+ inactive: v4.z
78980
+ .boolean()
78981
+ .optional()
78982
+ .describe('Whether the universe is inactive (hidden from active use)'),
78983
+ attributes: ServiceUniverseAttributesSchema.describe('Universe metadata (owner, label, description)'),
78984
+ access: ServiceUniverseAccessSchema.optional().describe('Access policy (users/groups)'),
78985
+ views: v4.z
78986
+ .array(ServiceUniverseViewSchema)
78987
+ .describe('The TAS + NASS sub-views defining the universe scope'),
78988
+ });
78989
+ /**
78990
+ * A summary row from `GET oi/v2/universe/getAllUniverseList`.
78991
+ */
78992
+ const ServiceUniverseListItemSchema = v4.z.looseObject({
78993
+ id: v4.z.string().describe('Universe (view) id, e.g. "VIEW1821"'),
78994
+ name: v4.z.string().optional().describe('Universe label'),
78995
+ description: v4.z.string().optional().describe('Universe description'),
78996
+ owner: v4.z
78997
+ .string()
78998
+ .optional()
78999
+ .describe("Owning product; service universes are 'OI'"),
79000
+ inactive: v4.z.boolean().optional().describe('Whether the universe is inactive'),
79001
+ });
79002
+ const ServiceUniverseListResponseSchema = v4.z
79003
+ .array(ServiceUniverseListItemSchema)
79004
+ .describe('All universes returned by getAllUniverseList');
79005
+ /**
79006
+ * A full service universe as returned by `POST views/queryView` (op:ID).
79007
+ * Loosely modeled — the detail response echoes the body plus server-resolved
79008
+ * fields (viewId, resolved access).
79009
+ */
79010
+ const ServiceUniverseSchema = v4.z.looseObject({
79011
+ viewId: v4.z.string().optional().describe('Server-assigned view id'),
79012
+ inactive: v4.z.boolean().optional().describe('Whether the universe is inactive'),
79013
+ attributes: ServiceUniverseAttributesSchema.optional().describe('Universe metadata'),
79014
+ access: ServiceUniverseAccessSchema.optional().describe('Access policy'),
79015
+ views: v4.z
79016
+ .array(v4.z.unknown())
79017
+ .optional()
79018
+ .describe('The universe sub-views (TAS/NASS)'),
79019
+ });
79020
+ /** Response wrapper for `POST views/queryView`. */
79021
+ const ServiceUniverseQueryResponseSchema = v4.z.looseObject({
79022
+ views: v4.z
79023
+ .array(ServiceUniverseSchema)
79024
+ .optional()
79025
+ .describe('Universes matching the query filter'),
79026
+ });
79027
+
79028
+ /**
79029
+ * Service for **Service Universes** — OI-owned universes (views) scoped to a
79030
+ * curated list of services, managed via the tenant `views/*` API plus the OI
79031
+ * universe-list endpoint. The "View sample" preview runs the SERVICE-op filter
79032
+ * as a TAS graph query.
79033
+ *
79034
+ * @example
79035
+ * ```ts
79036
+ * const svc = new ServiceUniverseService(dxSaasService, log);
79037
+ * const body = ServiceUniverseService.buildBody({ label: 'IoT', serviceNames: ['IoT RDS'] });
79038
+ * await svc.createServiceUniverse(body);
79039
+ * ```
79040
+ */
79041
+ class ServiceUniverseService {
79042
+ dxSaasService;
79043
+ log;
79044
+ constructor(dxSaasService, log) {
79045
+ this.dxSaasService = dxSaasService;
79046
+ this.log = log;
79047
+ }
79048
+ /**
79049
+ * Lists service universes (OI-owned) via
79050
+ * `GET oi/v2/universe/getAllUniverseList` (VIEWALL).
79051
+ *
79052
+ * @remarks Response-validated (mismatch → `log.warn`, raw still returned).
79053
+ */
79054
+ async listServiceUniverses() {
79055
+ const raw = await this.dxSaasService.oiGet('oi/v2/universe/getAllUniverseList');
79056
+ const result = ServiceUniverseListResponseSchema.safeParse(raw);
79057
+ if (!result.success) {
79058
+ this.log.warn('ServiceUniverseListResponse validation warning:', result.error.message);
79059
+ }
79060
+ const all = raw ?? [];
79061
+ return all.filter((u) => (u.owner ?? '').toUpperCase() === 'OI');
79062
+ }
79063
+ /**
79064
+ * Returns a single service universe by its view id via
79065
+ * `POST views/queryView` (op:ID). Throws if not exactly one match.
79066
+ */
79067
+ async getServiceUniverse(viewId) {
79068
+ const raw = await this.dxSaasService.tenantPost('views/queryView', {
79069
+ filter: { op: 'ID', ids: [viewId], input: { op: 'ALL' } },
79070
+ includeInactive: true,
79071
+ });
79072
+ const result = ServiceUniverseQueryResponseSchema.safeParse(raw);
79073
+ if (!result.success) {
79074
+ this.log.warn('ServiceUniverseQueryResponse validation warning:', result.error.message);
79075
+ }
79076
+ const views = raw.views ?? [];
79077
+ if (views.length !== 1) {
79078
+ throw new Error(`Expected exactly one universe with viewId '${viewId}', found ${views.length}.`);
79079
+ }
79080
+ return views[0];
79081
+ }
79082
+ /**
79083
+ * Creates a service universe via `POST views/view`.
79084
+ *
79085
+ * @throws ZodError if the body fails schema validation.
79086
+ */
79087
+ async createServiceUniverse(body) {
79088
+ ServiceUniverseBodySchema.parse(body);
79089
+ return this.dxSaasService.tenantPost('views/view', body);
79090
+ }
79091
+ /**
79092
+ * Updates a service universe via `PUT views/view/{viewId}`.
79093
+ *
79094
+ * @throws ZodError if the body fails schema validation.
79095
+ */
79096
+ async updateServiceUniverse(viewId, body) {
79097
+ ServiceUniverseBodySchema.parse(body);
79098
+ return this.dxSaasService.tenantPut(`views/view/${encodeURIComponent(viewId)}`, body);
79099
+ }
79100
+ /**
79101
+ * PUTs a full universe body to `views/view/{viewId}` **without** re-validating
79102
+ * it against the client schema. Used by the access commands, which round-trip
79103
+ * the universe's existing views verbatim (a UI-created universe may carry view
79104
+ * shapes our builder wouldn't reconstruct); only the `access` policy changes.
79105
+ */
79106
+ async putServiceUniverse(viewId, body) {
79107
+ return this.dxSaasService.tenantPut(`views/view/${encodeURIComponent(viewId)}`, body);
79108
+ }
79109
+ /** Deletes a service universe via `DELETE views/view/{viewId}`. */
79110
+ async deleteServiceUniverse(viewId) {
79111
+ return this.dxSaasService.tenantDelete(`views/view/${encodeURIComponent(viewId)}`);
79112
+ }
79113
+ /**
79114
+ * Checks which of the given service names actually exist on the tenant, via
79115
+ * `GET oi/v2/sa/services/{name}` per name. A 404 means the service does not
79116
+ * exist; any other error is re-thrown (so a transient failure is never
79117
+ * silently reported as "missing").
79118
+ *
79119
+ * @throws Error if any name could not be verified for a non-404 reason.
79120
+ */
79121
+ async resolveServiceNames(serviceNames) {
79122
+ const existing = [];
79123
+ const missing = [];
79124
+ const errored = [];
79125
+ await dist$1.PromisePool.for(serviceNames)
79126
+ .withConcurrency(5)
79127
+ .process(async (name) => {
79128
+ try {
79129
+ await this.dxSaasService.oiGet(`oi/v2/sa/services/${encodeURIComponent(name)}`, { subservices: false });
79130
+ existing.push(name);
79131
+ }
79132
+ catch (err) {
79133
+ if (err.httpCode === 404) {
79134
+ missing.push(name);
79135
+ }
79136
+ else {
79137
+ errored.push({ name, error: err });
79138
+ }
79139
+ }
79140
+ });
79141
+ if (errored.length > 0) {
79142
+ const first = errored[0].error;
79143
+ const reason = first instanceof Error ? first.message : String(first);
79144
+ throw new Error(`Could not verify ${errored.length} service name(s) (${errored
79145
+ .map((e) => e.name)
79146
+ .join(', ')}): ${reason}`);
79147
+ }
79148
+ return { existing, missing };
79149
+ }
79150
+ /**
79151
+ * "View sample": runs the SERVICE-scoped filter as a TAS graph query
79152
+ * (`POST tas/graph/query`) and returns the matching entities — the dry-run
79153
+ * preview of what a universe over `serviceNames` would contain.
79154
+ *
79155
+ * @remarks Response-validated (mismatch → `log.warn`, raw still returned).
79156
+ */
79157
+ async sampleServices(serviceNames, options = {}) {
79158
+ const query = {
79159
+ filter: {
79160
+ op: 'OR',
79161
+ input: [
79162
+ {
79163
+ op: 'SERVICE',
79164
+ values: serviceNames,
79165
+ includeServiceHierarchy: options.includeServiceHierarchy ?? true,
79166
+ excludeSubServices: options.excludeSubServices ?? false,
79167
+ },
79168
+ ],
79169
+ },
79170
+ authorizationView: 'VIEWALL',
79171
+ schema: 'SA',
79172
+ limit: options.limit ?? 200,
79173
+ };
79174
+ TasQuerySchema.parse(query);
79175
+ const raw = await this.dxSaasService.tenantPost('tas/graph/query', query);
79176
+ const result = TasGraphSchema.safeParse(raw);
79177
+ if (!result.success) {
79178
+ this.log.warn('ServiceUniverse sample (TasGraph) validation warning:', result.error.message);
79179
+ }
79180
+ return raw;
79181
+ }
79182
+ /**
79183
+ * Builds a service universe request body: a SERVICE-scoped TAS sub-view over
79184
+ * `serviceNames` (or an ALL view when empty) plus an ALL NASS sub-view. Mirrors
79185
+ * the shape the DXO2 Service Universe UI submits.
79186
+ */
79187
+ /** A user/group access grant: READ+WRITE, no attribute scoping (per current UX). */
79188
+ static accessEntry(name, canManage) {
79189
+ return {
79190
+ name,
79191
+ permissions: ['READ', 'WRITE'],
79192
+ canManage,
79193
+ attributeInclude: [],
79194
+ attributeExclude: [],
79195
+ };
79196
+ }
79197
+ /**
79198
+ * Returns a copy of `existing` with a user/group access grant added (or
79199
+ * updated in place if the principal already has a grant). Preserves the
79200
+ * universe's server-managed fields and views verbatim — only `access`
79201
+ * changes. Pass exactly one of `user` / `group`.
79202
+ */
79203
+ static withAccessEntry(existing, target) {
79204
+ const isGroup = !!target.group;
79205
+ const name = (target.user ?? target.group);
79206
+ const users = [...(existing.access?.users ?? [])];
79207
+ const groups = [...(existing.access?.groups ?? [])];
79208
+ const list = isGroup ? groups : users;
79209
+ const next = list.filter((e) => e.name !== name);
79210
+ next.push(this.accessEntry(name, target.canManage));
79211
+ const access = isGroup
79212
+ ? { users, groups: next }
79213
+ : { users: next, groups };
79214
+ return {
79215
+ ...existing,
79216
+ access,
79217
+ };
79218
+ }
79219
+ /**
79220
+ * Returns a copy of `existing` with a user/group access grant removed.
79221
+ * Preserves everything else verbatim. Pass exactly one of `user` / `group`.
79222
+ */
79223
+ static withoutAccessEntry(existing, target) {
79224
+ const isGroup = !!target.group;
79225
+ const name = (target.user ?? target.group);
79226
+ const users = (existing.access?.users ?? []).filter((e) => isGroup || e.name !== name);
79227
+ const groups = (existing.access?.groups ?? []).filter((e) => !isGroup || e.name !== name);
79228
+ return {
79229
+ ...existing,
79230
+ access: { users, groups },
79231
+ };
79232
+ }
79233
+ static buildBody(input) {
79234
+ const { label, description, serviceNames, inactive, includeServiceHierarchy, excludeSubServices, access, } = input;
79235
+ const tasFilter = serviceNames.length === 0
79236
+ ? { op: 'ALL' }
79237
+ : {
79238
+ op: 'SERVICE',
79239
+ values: serviceNames,
79240
+ includeServiceHierarchy: includeServiceHierarchy ?? true,
79241
+ excludeSubServices: excludeSubServices ?? false,
79242
+ };
79243
+ return {
79244
+ inactive: inactive ?? false,
79245
+ attributes: { owner: 'OI', label, description: description ?? '' },
79246
+ access: access ?? { groups: [], users: [] },
79247
+ views: [
79248
+ {
79249
+ type: 'tas',
79250
+ filter: {
79251
+ filter: tasFilter,
79252
+ includeMetricFilter: false,
79253
+ projection: 'NO_TYPE',
79254
+ projectionFilter: { attributes: ['name'] },
79255
+ },
79256
+ permissions: ['READ', 'WRITE'],
79257
+ },
79258
+ {
79259
+ type: 'nass',
79260
+ filter: {
79261
+ filter: { op: 'ALL' },
79262
+ serviceFilter: serviceNames.length === 0
79263
+ ? undefined
79264
+ : { op: 'SERVICE', values: serviceNames },
79265
+ },
79266
+ permissions: ['READ', 'WRITE'],
79267
+ },
79268
+ ],
79269
+ };
79270
+ }
79271
+ }
79272
+
78731
79273
  var NullSimpleLog = Logging.NullSimpleLog;
78732
79274
  /**
78733
79275
  * Creates and returns a container of all DX client services (ACC, alerts, alarms,
@@ -78762,6 +79304,7 @@ function createServiceMonolith(dxDoConfiguration, log = new NullSimpleLog()) {
78762
79304
  dxDataStoreNASSService: new DataStoreNASSService(dxSaaSService, log),
78763
79305
  dxDataStoreNASSQLService: new DataStoreNASSQLService(dxSaaSService, log),
78764
79306
  dxDataStoreTASService: new DataStoreTASService(dxSaaSService, log),
79307
+ dxServiceUniverseService: new ServiceUniverseService(dxSaaSService, log),
78765
79308
  dxEventService: new EventService(dxSaaSService, log),
78766
79309
  dxExperienceService: new ExperienceService(dxSaaSService, log),
78767
79310
  dxFeaturesService: new DataStoreFeaturesService(dxSaaSService, log),
@@ -78966,6 +79509,7 @@ exports.SLIService = SLIService;
78966
79509
  exports.SQLService = SQLService;
78967
79510
  exports.ServiceFilterSpecifierSchema = ServiceFilterSpecifierSchema;
78968
79511
  exports.ServiceService = ServiceService;
79512
+ exports.ServiceUniverseService = ServiceUniverseService;
78969
79513
  exports.SessionService = SessionService;
78970
79514
  exports.SimpleHTTPError = SimpleHTTPError;
78971
79515
  exports.SituationService = SituationService;