@tenonhq/dovetail-core 0.0.118 → 0.0.120

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.
@@ -560,17 +560,10 @@ class MultiScopeWatcherManager {
560
560
  try {
561
561
  const { defaultClient } = await Promise.resolve().then(() => __importStar(require("./snClient")));
562
562
  const client = defaultClient();
563
- // Create axios client directly to get update set details
564
- const axios = (await Promise.resolve().then(() => __importStar(require("axios")))).default;
565
- const { SN_USER = "", SN_PASSWORD = "", SN_INSTANCE = "" } = process.env;
566
- const axiosClient = axios.create({
567
- auth: {
568
- username: SN_USER,
569
- password: SN_PASSWORD
570
- },
571
- baseURL: SN_INSTANCE
572
- });
573
- const response = await axiosClient.get(`/api/now/table/sys_update_set/${updateSetId}`, {
563
+ // Reuse the shared client's axios instance — it carries the resolved
564
+ // auth mode (inbound API key or basic) so this read never drifts from
565
+ // the rest of the tool's transport.
566
+ const response = await client.client.get(`/api/now/table/sys_update_set/${updateSetId}`, {
574
567
  params: {
575
568
  sysparm_fields: "name,state,sys_id"
576
569
  }
@@ -301,10 +301,12 @@ async function initScopesCommand(args) {
301
301
  Logger_1.logger.info(`Using ${apiDelay}ms delay between API calls to prevent server overload`);
302
302
  }
303
303
  try {
304
- // First check if we have environment variables set
304
+ // First check if we have environment variables set. SN_USER stays
305
+ // required even in API-key mode (acting-user resolution); the password
306
+ // is optional once an inbound SN_API_KEY is set.
305
307
  if (!process.env.SN_USER ||
306
- !process.env.SN_PASSWORD ||
307
- !process.env.SN_INSTANCE) {
308
+ !process.env.SN_INSTANCE ||
309
+ (!process.env.SN_PASSWORD && !process.env.SN_API_KEY)) {
308
310
  try {
309
311
  let loginAnswers = await (0, wizard_1.getLoginInfo)();
310
312
  await (0, wizard_1.setupDotEnv)(loginAnswers);
@@ -314,9 +316,9 @@ async function initScopesCommand(args) {
314
316
  throw error;
315
317
  }
316
318
  if (!process.env.SN_USER ||
317
- !process.env.SN_PASSWORD ||
318
- !process.env.SN_INSTANCE) {
319
- Logger_1.logger.error("Missing ServiceNow credentials. Please ensure SN_USER, SN_PASSWORD, and SN_INSTANCE are set in your .env file");
319
+ !process.env.SN_INSTANCE ||
320
+ (!process.env.SN_PASSWORD && !process.env.SN_API_KEY)) {
321
+ Logger_1.logger.error("Missing ServiceNow credentials. Please ensure SN_USER, SN_INSTANCE, and SN_PASSWORD (or SN_API_KEY) are set in your .env file");
320
322
  throw new Error("ServiceNow credentials not configured");
321
323
  }
322
324
  }
@@ -371,12 +373,14 @@ async function watchAllScopesCommand(args) {
371
373
  (0, commands_1.setLogLevel)(args);
372
374
  var dashboardProcess = null;
373
375
  try {
374
- // First check if we have environment variables set
376
+ // First check if we have environment variables set. SN_USER stays
377
+ // required even in API-key mode; the password is optional once an
378
+ // inbound SN_API_KEY is set.
375
379
  if (!process.env.SN_USER ||
376
- !process.env.SN_PASSWORD ||
377
- !process.env.SN_INSTANCE) {
380
+ !process.env.SN_INSTANCE ||
381
+ (!process.env.SN_PASSWORD && !process.env.SN_API_KEY)) {
378
382
  let loginAnswers = await (0, wizard_1.getLoginInfo)();
379
- Logger_1.logger.error("Missing ServiceNow credentials. Please ensure SN_USER, SN_PASSWORD, and SN_INSTANCE are set in your .env file");
383
+ Logger_1.logger.error("Missing ServiceNow credentials. Please ensure SN_USER, SN_INSTANCE, and SN_PASSWORD (or SN_API_KEY) are set in your .env file");
380
384
  throw new Error("ServiceNow credentials not configured");
381
385
  }
382
386
  // Start dashboard unless --no-dashboard flag is set
@@ -98,6 +98,15 @@ exports.corePlugin = {
98
98
  },
99
99
  required: true,
100
100
  },
101
+ {
102
+ envKey: "SN_API_KEY",
103
+ prompt: {
104
+ type: "password",
105
+ message: "Inbound API key (optional, Enter to skip — becomes the default auth when set):",
106
+ mask: "*",
107
+ },
108
+ required: false,
109
+ },
101
110
  ],
102
111
  configure: [
103
112
  {
@@ -105,7 +114,7 @@ exports.corePlugin = {
105
114
  label: "Selecting ServiceNow applications",
106
115
  run: async (context) => {
107
116
  var baseUrl = instanceBaseUrl(context.env.SN_INSTANCE);
108
- var client = (0, snClient_1.snClient)(baseUrl, context.env.SN_USER, context.env.SN_PASSWORD);
117
+ var client = (0, snClient_1.snClient)(baseUrl, context.env.SN_USER, context.env.SN_PASSWORD, context.env.SN_API_KEY);
109
118
  Logger_1.logger.info("Fetching application list...");
110
119
  var apps = await (0, snClient_1.unwrapSNResponse)(client.getAppList());
111
120
  if (apps.length === 0) {
@@ -303,13 +312,16 @@ async function validateCoreLogin(context) {
303
312
  const instance = context.env.SN_INSTANCE;
304
313
  const user = context.env.SN_USER;
305
314
  const password = context.env.SN_PASSWORD;
306
- if (!instance || !user || !password) {
315
+ const apiKey = context.env.SN_API_KEY;
316
+ // SN_USER stays required even in API-key mode — the client's user-preference
317
+ // logic (current app / update set pinning) resolves the acting user from it.
318
+ if (!instance || !user || (!password && !apiKey)) {
307
319
  return "Missing required credentials";
308
320
  }
309
321
  const instanceUrl = normalizeInstance(instance);
310
322
  const baseUrl = instanceBaseUrl(instance);
311
323
  try {
312
- const client = (0, snClient_1.snClient)(baseUrl, user, password);
324
+ const client = (0, snClient_1.snClient)(baseUrl, user, password, apiKey);
313
325
  await (0, snClient_1.unwrapSNResponse)(client.getAppList());
314
326
  context.env.SN_INSTANCE = instanceUrl;
315
327
  return true;
@@ -321,7 +333,7 @@ async function validateCoreLogin(context) {
321
333
  return "Instance not found — check the URL (got: " + instanceUrl + ")";
322
334
  }
323
335
  if (status === 401 || msg.includes("401")) {
324
- return "Invalid username or password.";
336
+ return "Invalid credentials — check the username/password (or the API key and its REST API access policies).";
325
337
  }
326
338
  if (status === 403 || msg.includes("403")) {
327
339
  return "Access denied — user may lack required roles.";
@@ -95,11 +95,13 @@ function getUpdateSetConfig() {
95
95
  }
96
96
  const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
97
97
  function requireCreds() {
98
- const { SN_USER = "", SN_PASSWORD = "", SN_INSTANCE = "" } = process.env;
99
- if (!SN_USER || !SN_PASSWORD || !SN_INSTANCE) {
100
- throw new Error("Missing ServiceNow credentials. Ensure SN_INSTANCE, SN_USER, and SN_PASSWORD are set in your .env file or environment.");
98
+ const { SN_USER = "", SN_PASSWORD = "", SN_INSTANCE = "", SN_API_KEY = "", } = process.env;
99
+ // SN_USER stays required even in API-key mode (acting-user resolution);
100
+ // the password becomes optional once an inbound API key is set.
101
+ if (!SN_USER || !SN_INSTANCE || (!SN_PASSWORD && !SN_API_KEY)) {
102
+ throw new Error("Missing ServiceNow credentials. Ensure SN_INSTANCE, SN_USER, and SN_PASSWORD (or SN_API_KEY) are set in your .env file or environment.");
101
103
  }
102
- return { SN_USER, SN_PASSWORD, SN_INSTANCE };
104
+ return { SN_USER, SN_PASSWORD, SN_INSTANCE, SN_API_KEY };
103
105
  }
104
106
  function normalizeInstance(instance) {
105
107
  return instance.replace(/^https?:\/\//, "").replace(/\/$/, "");
@@ -158,6 +160,7 @@ async function buildSchemaDiffs(options) {
158
160
  instance: creds.SN_INSTANCE,
159
161
  username: creds.SN_USER,
160
162
  password: creds.SN_PASSWORD,
163
+ apiKey: creds.SN_API_KEY,
161
164
  outputDir: tmpDir,
162
165
  scopes,
163
166
  });
@@ -46,11 +46,13 @@ const path_1 = __importDefault(require("path"));
46
46
  const os_1 = __importDefault(require("os"));
47
47
  const fs_1 = require("fs");
48
48
  function requireCreds() {
49
- const { SN_USER = "", SN_PASSWORD = "", SN_INSTANCE = "" } = process.env;
50
- if (!SN_USER || !SN_PASSWORD || !SN_INSTANCE) {
51
- throw new Error("Missing ServiceNow credentials. Ensure SN_INSTANCE, SN_USER, and SN_PASSWORD are set in your .env file or environment.");
49
+ const { SN_USER = "", SN_PASSWORD = "", SN_INSTANCE = "", SN_API_KEY = "", } = process.env;
50
+ // SN_USER stays required even in API-key mode (acting-user resolution);
51
+ // the password becomes optional once an inbound API key is set.
52
+ if (!SN_USER || !SN_INSTANCE || (!SN_PASSWORD && !SN_API_KEY)) {
53
+ throw new Error("Missing ServiceNow credentials. Ensure SN_INSTANCE, SN_USER, and SN_PASSWORD (or SN_API_KEY) are set in your .env file or environment.");
52
54
  }
53
- return { SN_USER, SN_PASSWORD, SN_INSTANCE };
55
+ return { SN_USER, SN_PASSWORD, SN_INSTANCE, SN_API_KEY };
54
56
  }
55
57
  function normalizeInstance(instance) {
56
58
  return instance.replace(/^https?:\/\//, "").replace(/\/$/, "");
@@ -74,7 +76,7 @@ function outputDirFor(args) {
74
76
  return args.output ? path_1.default.resolve(args.output) : path_1.default.join(rootDir, "schema");
75
77
  }
76
78
  async function schemaPullCommand(args) {
77
- const { SN_USER, SN_PASSWORD, SN_INSTANCE } = requireCreds();
79
+ const { SN_USER, SN_PASSWORD, SN_INSTANCE, SN_API_KEY } = requireCreds();
78
80
  const scopes = resolveScopes(args);
79
81
  const outputDir = outputDirFor(args);
80
82
  try {
@@ -82,6 +84,7 @@ async function schemaPullCommand(args) {
82
84
  instance: SN_INSTANCE,
83
85
  username: SN_USER,
84
86
  password: SN_PASSWORD,
87
+ apiKey: SN_API_KEY,
85
88
  outputDir,
86
89
  scopes,
87
90
  });
@@ -119,6 +122,7 @@ async function resolveRefDir(options) {
119
122
  instance: creds.SN_INSTANCE,
120
123
  username: creds.SN_USER,
121
124
  password: creds.SN_PASSWORD,
125
+ apiKey: creds.SN_API_KEY,
122
126
  outputDir: tmpDir,
123
127
  scopes,
124
128
  });
package/dist/snClient.js CHANGED
@@ -243,17 +243,26 @@ const processPushResponse = (response, recSummary) => {
243
243
  };
244
244
  };
245
245
  exports.processPushResponse = processPushResponse;
246
- const snClient = (baseURL, username, password) => {
246
+ const snClient = (baseURL, username, password, apiKey) => {
247
247
  const jar = new tough_cookie_1.CookieJar();
248
+ // When an inbound API key is supplied it is the default auth mode: the
249
+ // x-sn-apikey header replaces basic auth entirely. Requires key-enabled
250
+ // REST API access policies on the instance.
251
+ const baseHeaders = {
252
+ "Content-Type": "application/json",
253
+ };
254
+ if (apiKey) {
255
+ baseHeaders["x-sn-apikey"] = apiKey;
256
+ }
248
257
  const rawAxios = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({
249
258
  withCredentials: true,
250
- auth: {
251
- username,
252
- password,
253
- },
254
- headers: {
255
- "Content-Type": "application/json",
256
- },
259
+ auth: apiKey
260
+ ? undefined
261
+ : {
262
+ username,
263
+ password,
264
+ },
265
+ headers: baseHeaders,
257
266
  baseURL,
258
267
  jar,
259
268
  }));
@@ -540,8 +549,8 @@ const defaultClient = () => {
540
549
  if (internalClient) {
541
550
  return internalClient;
542
551
  }
543
- const { SN_USER = "", SN_PASSWORD = "", SN_INSTANCE = "" } = process.env;
544
- internalClient = (0, exports.snClient)(`https://${SN_INSTANCE}/`, SN_USER, SN_PASSWORD);
552
+ const { SN_USER = "", SN_PASSWORD = "", SN_INSTANCE = "", SN_API_KEY = "", } = process.env;
553
+ internalClient = (0, exports.snClient)(`https://${SN_INSTANCE}/`, SN_USER, SN_PASSWORD, SN_API_KEY);
545
554
  return internalClient;
546
555
  };
547
556
  exports.defaultClient = defaultClient;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ // Auth-mode tests for snClient: an inbound API key (4th param) becomes the
3
+ // x-sn-apikey header on the real axios instance and basic auth is NOT set;
4
+ // without a key the existing basic-auth behavior is unchanged. Also covers
5
+ // defaultClient() picking SN_API_KEY out of the environment.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ var mockLogger = {
8
+ info: jest.fn(),
9
+ debug: jest.fn(),
10
+ warn: jest.fn(),
11
+ error: jest.fn(),
12
+ getLogLevel: function () { return "debug"; },
13
+ };
14
+ jest.mock("../Logger", function () {
15
+ return { logger: mockLogger };
16
+ });
17
+ jest.mock("../FileLogger", function () {
18
+ return { fileLogger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } };
19
+ });
20
+ jest.mock("../genericUtils", function () {
21
+ return { wait: jest.fn().mockResolvedValue(undefined) };
22
+ });
23
+ const snClient_1 = require("../snClient");
24
+ describe("snClient — auth mode", function () {
25
+ it("key mode: sets x-sn-apikey header and no basic auth", function () {
26
+ var c = (0, snClient_1.snClient)("https://x.service-now.com/", "user", "pass", "key-123");
27
+ expect(c.client.defaults.headers["x-sn-apikey"]).toBe("key-123");
28
+ expect(c.client.defaults.auth).toBeUndefined();
29
+ });
30
+ it("basic mode: sets basic auth and no x-sn-apikey header", function () {
31
+ var c = (0, snClient_1.snClient)("https://x.service-now.com/", "user", "pass");
32
+ expect(c.client.defaults.auth).toEqual({ username: "user", password: "pass" });
33
+ expect(c.client.defaults.headers["x-sn-apikey"]).toBeUndefined();
34
+ });
35
+ it("empty-string key falls back to basic auth", function () {
36
+ var c = (0, snClient_1.snClient)("https://x.service-now.com/", "user", "pass", "");
37
+ expect(c.client.defaults.auth).toEqual({ username: "user", password: "pass" });
38
+ expect(c.client.defaults.headers["x-sn-apikey"]).toBeUndefined();
39
+ });
40
+ });
package/dist/wizard.js CHANGED
@@ -24,17 +24,25 @@ async function getLoginInfo() {
24
24
  name: "password",
25
25
  message: "What is your password on that instance?",
26
26
  },
27
+ {
28
+ type: "password",
29
+ name: "apiKey",
30
+ message: "Inbound API key (optional, Enter to skip — becomes the default auth when set)?",
31
+ },
27
32
  ]);
28
33
  }
29
34
  async function setupDotEnv(answers) {
30
35
  process.env.SN_USER = answers.username;
31
36
  process.env.SN_PASSWORD = answers.password;
32
37
  process.env.SN_INSTANCE = answers.instance;
33
- (0, FileUtils_1.writeEnvVars)({
34
- vars: [
35
- { key: "SN_USER", value: answers.username },
36
- { key: "SN_PASSWORD", value: answers.password },
37
- { key: "SN_INSTANCE", value: answers.instance },
38
- ],
39
- });
38
+ var vars = [
39
+ { key: "SN_USER", value: answers.username },
40
+ { key: "SN_PASSWORD", value: answers.password },
41
+ { key: "SN_INSTANCE", value: answers.instance },
42
+ ];
43
+ if (answers.apiKey) {
44
+ process.env.SN_API_KEY = answers.apiKey;
45
+ vars.push({ key: "SN_API_KEY", value: answers.apiKey });
46
+ }
47
+ (0, FileUtils_1.writeEnvVars)({ vars: vars });
40
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-core",
3
- "version": "0.0.118",
3
+ "version": "0.0.120",
4
4
  "description": "Next-gen file syncer",
5
5
  "license": "GPL-3.0",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "node": ">=22"
23
23
  },
24
24
  "devDependencies": {
25
- "@tenonhq/dovetail-types": "~0.0.20",
25
+ "@tenonhq/dovetail-types": "~0.0.21",
26
26
  "@types/dotenv": "^6.1.1",
27
27
  "@types/inquirer": "^8.2.10",
28
28
  "@types/jest": "^29.5.5",
@@ -38,9 +38,9 @@
38
38
  "typescript": "^5.2.2"
39
39
  },
40
40
  "dependencies": {
41
- "@tenonhq/dovetail-clickup": "~0.0.13",
42
- "@tenonhq/dovetail-schema": "~0.0.12",
43
- "axios": "^1.5.1",
41
+ "@tenonhq/dovetail-clickup": "~0.0.14",
42
+ "@tenonhq/dovetail-schema": "~0.0.14",
43
+ "axios": "^1.20.0",
44
44
  "axios-cookiejar-support": "^4.0.7",
45
45
  "axios-rate-limit": "^1.3.0",
46
46
  "chalk": "^4.1.2",