@jacebenson/jsn 4.3.11 → 4.3.12
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/bin/jsn.js +7 -1
- package/package.json +3 -3
- package/src/app.js +10 -23
- package/src/auth/browser-session.js +21 -0
- package/src/auth/credential-state.js +124 -0
- package/src/auth/credential-store.js +177 -0
- package/src/auth/oauth.js +66 -0
- package/src/auth.js +11 -378
- package/src/cli.js +3 -1
- package/src/commands/docs/db.js +22 -2
- package/src/commands/docs/docs.js +13 -13
- package/src/commands/docs/embed.js +33 -33
- package/src/commands/docs/ingest.js +58 -58
- package/src/commands/docs/refresh.js +91 -91
- package/src/commands/docs/search.js +108 -108
- package/src/commands/docs/serve.js +4 -1
- package/src/commands/setup.js +27 -25
- package/src/commands/version.js +16 -2
- package/src/frontmatter.js +5 -3
- package/src/helpers.js +1 -1
- package/src/paginated-search.js +162 -161
- package/src/prompts.js +15 -9
- package/src/sdk.js +79 -92
package/src/sdk.js
CHANGED
|
@@ -50,6 +50,7 @@ export class SDKClient {
|
|
|
50
50
|
this.timeout = opts.timeout || DEFAULT_TIMEOUT;
|
|
51
51
|
this.domain = opts.domain || '';
|
|
52
52
|
this.oauthCookies = null;
|
|
53
|
+
this._sessionPromise = null;
|
|
53
54
|
this.sessionUserToken = '';
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -94,74 +95,63 @@ export class SDKClient {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
// Keep the timeout alive through body consumption for text/JSON requests.
|
|
99
|
+
// Raw-response callers retain their existing timeout boundary at headers.
|
|
100
|
+
async _withAuthenticatedRequest(endpoint, opts, {
|
|
101
|
+
timeout = this.timeout, prepare, skipOAuthSession = false, normalizeErrors = false,
|
|
102
|
+
}, consume = (response) => response) {
|
|
100
103
|
const controller = new AbortController();
|
|
101
104
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
102
|
-
|
|
103
105
|
try {
|
|
104
|
-
const req = new Request(endpoint, {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
const req = new Request(endpoint, { ...opts, signal: controller.signal });
|
|
107
|
+
if (prepare) prepare(req);
|
|
108
|
+
await this._setAuth(req, { skipOAuthSession });
|
|
109
|
+
return await consume(await fetch(req));
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (normalizeErrors) {
|
|
112
|
+
if (err.name === 'AbortError') throw errNetwork(new Error('Request timed out'));
|
|
113
|
+
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT') {
|
|
114
|
+
throw errNetwork(err);
|
|
115
|
+
}
|
|
114
116
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
throw err;
|
|
118
|
+
} finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
118
122
|
|
|
119
|
-
|
|
123
|
+
async request(endpoint, opts = {}) {
|
|
124
|
+
const { timeout = this.timeout, readOnly = false, touchLastSeen, ...requestOptions } = opts;
|
|
125
|
+
if (readOnly && (requestOptions.method || 'GET').toUpperCase() !== 'GET') throw new Error('Read-only SDK requests require GET');
|
|
126
|
+
return this._withAuthenticatedRequest(endpoint, requestOptions, {
|
|
127
|
+
timeout,
|
|
128
|
+
normalizeErrors: true,
|
|
129
|
+
prepare: (req) => {
|
|
130
|
+
req.headers.set('Accept', 'application/json');
|
|
131
|
+
if (requestOptions.body && typeof requestOptions.body === 'string') {
|
|
132
|
+
req.headers.set('Content-Type', 'application/json');
|
|
133
|
+
}
|
|
134
|
+
if (this.domain) req.headers.set('X-Now-Domain', this.domain);
|
|
135
|
+
},
|
|
136
|
+
}, async (resp) => {
|
|
120
137
|
const body = await resp.text();
|
|
138
|
+
if (!resp.ok) throw errAPI(resp.status, body || resp.statusText);
|
|
139
|
+
if (resp.status === 204 || body === '') return null;
|
|
121
140
|
|
|
122
|
-
|
|
123
|
-
throw errAPI(resp.status, body || resp.statusText);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (resp.status === 204 || body === '') {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Stamp last_seen on successful API call
|
|
141
|
+
// Preserve read-only credential-write suppression and the existing empty-body behavior.
|
|
131
142
|
try {
|
|
132
|
-
// readOnly suppresses the credential last-seen write unconditionally: a
|
|
133
|
-
// caller passing touchLastSeen cannot override it (GLOSSARY contract).
|
|
134
143
|
const shouldTouch = readOnly ? false : (touchLastSeen ?? true);
|
|
135
144
|
if (shouldTouch && this.authProvider && typeof this.authProvider.touchLastSeen === 'function') {
|
|
136
145
|
this.authProvider.touchLastSeen(this.baseURL);
|
|
137
146
|
}
|
|
138
147
|
} catch { /* non-fatal */ }
|
|
139
|
-
|
|
140
148
|
return JSON.parse(body);
|
|
141
|
-
}
|
|
142
|
-
if (err.name === 'AbortError') {
|
|
143
|
-
throw errNetwork(new Error('Request timed out'));
|
|
144
|
-
}
|
|
145
|
-
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT') {
|
|
146
|
-
throw errNetwork(err);
|
|
147
|
-
}
|
|
148
|
-
throw err;
|
|
149
|
-
} finally {
|
|
150
|
-
clearTimeout(timer);
|
|
151
|
-
}
|
|
149
|
+
});
|
|
152
150
|
}
|
|
153
151
|
|
|
154
|
-
//
|
|
152
|
+
// Authentication bootstrap bypasses session warming to avoid recursion.
|
|
155
153
|
async _fetchWithAuth(endpoint, opts = {}) {
|
|
156
|
-
|
|
157
|
-
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
158
|
-
try {
|
|
159
|
-
const req = new Request(endpoint, { ...opts, signal: controller.signal });
|
|
160
|
-
await this._setAuth(req, { skipOAuthSession: true });
|
|
161
|
-
return await fetch(req);
|
|
162
|
-
} finally {
|
|
163
|
-
clearTimeout(timer);
|
|
164
|
-
}
|
|
154
|
+
return this._withAuthenticatedRequest(endpoint, opts, { skipOAuthSession: true });
|
|
165
155
|
}
|
|
166
156
|
|
|
167
157
|
// Public raw-response seam for callers that must inspect non-2xx bodies.
|
|
@@ -170,38 +160,18 @@ export class SDKClient {
|
|
|
170
160
|
}
|
|
171
161
|
|
|
172
162
|
async rawRequest(endpoint, opts = {}) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (opts.body && typeof opts.body === 'string') {
|
|
182
|
-
req.headers.set('Content-Type', opts.headers?.['Content-Type'] || 'application/x-www-form-urlencoded');
|
|
183
|
-
}
|
|
184
|
-
await this._setAuth(req);
|
|
185
|
-
|
|
186
|
-
const resp = await fetch(req);
|
|
163
|
+
return this._withAuthenticatedRequest(endpoint, opts, {
|
|
164
|
+
normalizeErrors: true,
|
|
165
|
+
prepare: (req) => {
|
|
166
|
+
if (opts.body && typeof opts.body === 'string') {
|
|
167
|
+
req.headers.set('Content-Type', opts.headers?.['Content-Type'] || 'application/x-www-form-urlencoded');
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
}, async (resp) => {
|
|
187
171
|
const body = await resp.text();
|
|
188
|
-
|
|
189
|
-
if (!resp.ok) {
|
|
190
|
-
throw errAPI(resp.status, body || resp.statusText);
|
|
191
|
-
}
|
|
192
|
-
|
|
172
|
+
if (!resp.ok) throw errAPI(resp.status, body || resp.statusText);
|
|
193
173
|
return body;
|
|
194
|
-
}
|
|
195
|
-
if (err.name === 'AbortError') {
|
|
196
|
-
throw errNetwork(new Error('Request timed out'));
|
|
197
|
-
}
|
|
198
|
-
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT') {
|
|
199
|
-
throw errNetwork(err);
|
|
200
|
-
}
|
|
201
|
-
throw err;
|
|
202
|
-
} finally {
|
|
203
|
-
clearTimeout(timer);
|
|
204
|
-
}
|
|
174
|
+
});
|
|
205
175
|
}
|
|
206
176
|
|
|
207
177
|
async getSkippedUpdateBaseline(sysID) {
|
|
@@ -791,18 +761,26 @@ export class SDKClient {
|
|
|
791
761
|
|
|
792
762
|
const result = { _record: records[0] };
|
|
793
763
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
764
|
+
// Four workers bound instance load while overlapping independent reads.
|
|
765
|
+
let next = 0;
|
|
766
|
+
const values = new Array(related.length);
|
|
767
|
+
await Promise.all(Array.from({ length: Math.min(4, related.length) }, async () => {
|
|
768
|
+
while (next < related.length) {
|
|
769
|
+
const index = next++;
|
|
770
|
+
const rel = related[index];
|
|
771
|
+
try {
|
|
772
|
+
const relParams = new URLSearchParams();
|
|
773
|
+
relParams.set('sysparm_display_value', 'all');
|
|
774
|
+
relParams.set('sysparm_fields', (rel.fields || []).join(','));
|
|
775
|
+
relParams.set('sysparm_query', `${rel.queryField}=${rel.queryValue}`);
|
|
776
|
+
relParams.set('sysparm_limit', '100');
|
|
777
|
+
values[index] = await this.list(rel.table, relParams);
|
|
778
|
+
} catch {
|
|
779
|
+
values[index] = [];
|
|
780
|
+
}
|
|
804
781
|
}
|
|
805
|
-
}
|
|
782
|
+
}));
|
|
783
|
+
related.forEach((rel, index) => { result[rel.displayAs] = values[index]; });
|
|
806
784
|
|
|
807
785
|
return result;
|
|
808
786
|
}
|
|
@@ -859,6 +837,15 @@ export class SDKClient {
|
|
|
859
837
|
|
|
860
838
|
async _warmSession() {
|
|
861
839
|
if (this.oauthCookies) return this.oauthCookies;
|
|
840
|
+
if (!this._sessionPromise) {
|
|
841
|
+
this._sessionPromise = this._initializeSession().finally(() => {
|
|
842
|
+
this._sessionPromise = null;
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
return this._sessionPromise;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async _initializeSession() {
|
|
862
849
|
try {
|
|
863
850
|
const creds = await this.authProvider.getCredentials();
|
|
864
851
|
if (creds.auth_method === 'oauth') {
|