@unboundcx/sdk 4.13.110 → 4.13.112-security.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/base.js CHANGED
@@ -13,6 +13,8 @@
13
13
  * npm install mime-types
14
14
  */
15
15
 
16
+ import { handleUnauthorized } from './lib/refreshInterceptor.js';
17
+
16
18
  const requestBySdk = new WeakMap();
17
19
  const SDK_REQUEST = Symbol.for('unbound.sdk.request');
18
20
 
@@ -49,12 +51,33 @@ export class BaseSDK {
49
51
  this.fwRequestId = arguments[3];
50
52
  } else {
51
53
  // New object-based parameters
52
- const { namespace, callId, token, fwRequestId, baseURL } = options;
54
+ const {
55
+ namespace,
56
+ callId,
57
+ token,
58
+ fwRequestId,
59
+ baseURL,
60
+ autoRefresh,
61
+ onUnauthorized,
62
+ refreshToken,
63
+ } = options;
53
64
  this.namespace = namespace || process?.env?.namespace;
54
65
  this.callId = callId;
55
66
  this.token = token;
56
67
  this.fwRequestId = fwRequestId;
57
68
  this._constructorBaseURL = baseURL;
69
+ // api#222 -- opt-in only (default false): existing callers get no
70
+ // behavior change. When on, a 401 from any non-/login* endpoint
71
+ // triggers one single-flight refresh + one retry (lib/refreshInterceptor.js).
72
+ this._autoRefresh = autoRefresh === true;
73
+ this._onUnauthorized =
74
+ typeof onUnauthorized === 'function' ? onUnauthorized : null;
75
+ // Bearer-delivery clients only: the raw refresh token, so autoRefresh
76
+ // has something to send to POST /login/refresh (cookie clients rely
77
+ // on the browser's own refreshToken cookie and never need this).
78
+ // Seeded via this option or setRefreshToken(); updated in place on
79
+ // every rotation by lib/refreshInterceptor.js.
80
+ this._refreshToken = refreshToken;
58
81
  }
59
82
  this.baseURL;
60
83
  this.transports = new Map();
@@ -104,6 +127,10 @@ export class BaseSDK {
104
127
  this.token = token;
105
128
  }
106
129
 
130
+ setRefreshToken(refreshToken) {
131
+ this._refreshToken = refreshToken;
132
+ }
133
+
107
134
  setNamespace(namespace) {
108
135
  this.namespace = namespace;
109
136
  const defaultDomain = 'api.unbound.cx';
@@ -278,6 +305,7 @@ export class BaseSDK {
278
305
  params,
279
306
  returnRawResponse,
280
307
  startTime,
308
+ forceFetch,
281
309
  );
282
310
  }
283
311
  } else {
@@ -292,6 +320,7 @@ export class BaseSDK {
292
320
  params,
293
321
  returnRawResponse,
294
322
  startTime,
323
+ forceFetch,
295
324
  );
296
325
  }
297
326
 
@@ -307,6 +336,7 @@ export class BaseSDK {
307
336
  method,
308
337
  endpoint,
309
338
  duration,
339
+ { originalParams: params, forceFetch },
310
340
  );
311
341
  }
312
342
 
@@ -357,6 +387,7 @@ export class BaseSDK {
357
387
  params = {},
358
388
  returnRawResponse = false,
359
389
  startTime = Date.now(),
390
+ forceFetch = false,
360
391
  ) {
361
392
  const { body, query, headers = {} } = params;
362
393
 
@@ -438,10 +469,20 @@ export class BaseSDK {
438
469
  return response;
439
470
  }
440
471
 
441
- return this._processResponse(response, 'https', method, endpoint, duration);
472
+ return this._processResponse(response, 'https', method, endpoint, duration, {
473
+ originalParams: params,
474
+ forceFetch,
475
+ });
442
476
  }
443
477
 
444
- async _processResponse(response, transport, method, endpoint, duration = 0) {
478
+ async _processResponse(
479
+ response,
480
+ transport,
481
+ method,
482
+ endpoint,
483
+ duration = 0,
484
+ retryCtx = {},
485
+ ) {
445
486
  // Check if the response indicates an HTTP error
446
487
  // These are API/configuration errors, not transport failures
447
488
 
@@ -518,6 +559,27 @@ export class BaseSDK {
518
559
  );
519
560
  }
520
561
 
562
+ // api#222 -- opt-in single-flight refresh + one retry on 401. Off by
563
+ // default (this._autoRefresh set only via the `autoRefresh` ctor
564
+ // option); the retry's own params carry `__skipAutoRefresh` so a 401
565
+ // on the RETRY itself always falls through to onUnauthorized instead
566
+ // of looping.
567
+ if (response.status === 401 && this._autoRefresh) {
568
+ return handleUnauthorized(this, {
569
+ status: response.status,
570
+ endpoint,
571
+ alreadyRetried: retryCtx?.originalParams?.__skipAutoRefresh === true,
572
+ originalError: httpError,
573
+ retry: () =>
574
+ this.#request(
575
+ endpoint,
576
+ method,
577
+ { ...(retryCtx.originalParams || {}), __skipAutoRefresh: true },
578
+ retryCtx.forceFetch,
579
+ ),
580
+ });
581
+ }
582
+
521
583
  throw httpError;
522
584
  }
523
585
 
package/index.js CHANGED
@@ -38,6 +38,7 @@ import { FaxService } from './services/fax.js';
38
38
  import { DocumentsService } from './services/documents.js';
39
39
  import { EsignService } from './services/esign.js';
40
40
  import { PermissionsService } from './services/permissions.js';
41
+ import { LicensesService } from './services/licenses.js';
41
42
  import { UsersService } from './services/users.js';
42
43
  import { TriggersService } from './services/triggers.js';
43
44
  import { RecentsService } from './services/recents.js';
@@ -138,6 +139,7 @@ class UnboundSDK extends BaseSDK {
138
139
  this.documents = new DocumentsService(this);
139
140
  this.esign = new EsignService(this);
140
141
  this.permissions = new PermissionsService(this);
142
+ this.licenses = new LicensesService(this);
141
143
  this.users = new UsersService(this);
142
144
  this.reporting = new ReportingService(this);
143
145
  this.triggers = new TriggersService(this);
@@ -344,6 +346,7 @@ export { KnowledgeBaseService } from './services/knowledgeBase.js';
344
346
  export { FaxService } from './services/fax.js';
345
347
  export { EsignService, EsignPublicService } from './services/esign.js';
346
348
  export { PermissionsService } from './services/permissions.js';
349
+ export { LicensesService } from './services/licenses.js';
347
350
  export { UsersService } from './services/users.js';
348
351
  export { ReportingService } from './services/reporting.js';
349
352
  export { RecentsService } from './services/recents.js';
@@ -0,0 +1,106 @@
1
+ // api#222 client contract -- single-flight 401 -> refresh -> retry-once.
2
+ // base.js's `#request` funnels BOTH the Socket.IO transport and the plain
3
+ // HTTP fallback through the same `_processResponse` (see base.js), so this
4
+ // one hook covers every transport without a per-transport copy.
5
+ //
6
+ // Usage (base.js only -- app code never imports this directly):
7
+ // handleUnauthorized(sdk, { status, endpoint, retry, originalError })
8
+ //
9
+ // `retry` is a caller-supplied () => Promise that re-issues the ORIGINAL
10
+ // request. It has to come from the caller because #request is a private
11
+ // class field method -- this module can't call it directly.
12
+
13
+ // Keyed per SDK instance: a shared module-level variable would let one
14
+ // SDK instance's refresh starve a concurrent, unrelated SDK instance's own
15
+ // 401 recovery (a different user/session in the same Node process -- the
16
+ // API itself increasingly consumes this SDK server-side). Each instance
17
+ // still single-flights its own concurrent 401s against itself.
18
+ const inFlightRefreshBySdk = new WeakMap();
19
+
20
+ function isLoginRoute(endpoint) {
21
+ return typeof endpoint === 'string' && endpoint.startsWith('/login');
22
+ }
23
+
24
+ export function shouldAttemptRefresh(sdk, status, endpoint) {
25
+ return (
26
+ status === 401 &&
27
+ sdk?._autoRefresh === true &&
28
+ !isLoginRoute(endpoint) &&
29
+ typeof sdk?.login?.refresh === 'function'
30
+ );
31
+ }
32
+
33
+ async function runRefresh(sdk) {
34
+ let inFlight = inFlightRefreshBySdk.get(sdk);
35
+ if (!inFlight) {
36
+ // Bearer clients (constructed with a raw token, no browser cookie jar)
37
+ // have to hand their raw refresh token back on every call -- the SDK
38
+ // never gets one implicitly the way a cookie client's browser does.
39
+ // sdk._refreshToken is set from login/refresh's own response below, or
40
+ // may be seeded via sdk.setRefreshToken() by a caller that obtained it
41
+ // out of band. Cookie clients simply have no stored value: `undefined`
42
+ // is fine, refresh() omits body.refreshToken and relies on the cookie.
43
+ inFlight = sdk.login
44
+ .refresh(sdk._refreshToken)
45
+ .then((result) => {
46
+ // Bearer clients get a new access token AND a new (rotated) refresh
47
+ // token back in the body; cookie clients rely on the Set-Cookie
48
+ // pair the refresh response already sent -- there is no token
49
+ // string to apply to the SDK instance for them.
50
+ if (result?.token) sdk.setToken(result.token);
51
+ if (result?.refreshToken) sdk._refreshToken = result.refreshToken;
52
+ return result;
53
+ })
54
+ .finally(() => {
55
+ inFlightRefreshBySdk.delete(sdk);
56
+ });
57
+ inFlightRefreshBySdk.set(sdk, inFlight);
58
+ }
59
+ return inFlight;
60
+ }
61
+
62
+ function notifyUnauthorized(sdk, err) {
63
+ if (typeof sdk?._onUnauthorized !== 'function') return;
64
+ try {
65
+ sdk._onUnauthorized(err);
66
+ } catch (callbackErr) {
67
+ console.error(
68
+ 'refreshInterceptor :: onUnauthorized callback threw ::',
69
+ callbackErr,
70
+ );
71
+ }
72
+ }
73
+
74
+ /**
75
+ * @param {object} sdk
76
+ * @param {object} ctx
77
+ * @param {number} ctx.status
78
+ * @param {string} ctx.endpoint
79
+ * @param {() => Promise<any>} ctx.retry
80
+ * @param {Error} ctx.originalError -- rethrown whenever refresh isn't
81
+ * attempted, fails, or the retry itself still comes back 401 (never loop
82
+ * past one retry).
83
+ * @param {boolean} [ctx.alreadyRetried]
84
+ */
85
+ export async function handleUnauthorized(
86
+ sdk,
87
+ { status, endpoint, retry, originalError, alreadyRetried = false },
88
+ ) {
89
+ if (alreadyRetried || !shouldAttemptRefresh(sdk, status, endpoint)) {
90
+ throw originalError;
91
+ }
92
+
93
+ try {
94
+ await runRefresh(sdk);
95
+ } catch (refreshErr) {
96
+ notifyUnauthorized(sdk, originalError);
97
+ throw originalError;
98
+ }
99
+
100
+ try {
101
+ return await retry();
102
+ } catch (retryErr) {
103
+ if (retryErr?.status === 401) notifyUnauthorized(sdk, retryErr);
104
+ throw retryErr;
105
+ }
106
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboundcx/sdk",
3
- "version": "4.13.110",
3
+ "version": "4.13.112-security.0",
4
4
  "description": "Official JavaScript SDK for the Unbound API - A comprehensive toolkit for integrating with Unbound's communication, AI, and data management services",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -40,6 +40,7 @@
40
40
  "*.js",
41
41
  "services/**/*.js",
42
42
  "transports/**/*.js",
43
+ "lib/**/*.js",
43
44
  "types/**/*.d.ts",
44
45
  "proto/**/*.proto",
45
46
  "schemas/**/*.js",
@@ -0,0 +1,330 @@
1
+ import { internalRequest } from '../base.js';
2
+
3
+ /**
4
+ * Entitlements / license administration (W2, plan §2 frozen contract).
5
+ * Tenant-admin methods hit `/permissions/*` (admin:license:manage /
6
+ * admin:user:manage, real guard in the controller). Brand-owner methods
7
+ * hit `/permissions/brand/*` (requireBrandOwner + brandId ownership
8
+ * check, 2D). Neither is tenant-writable for seats/overrides/AI --
9
+ * those live under `/internal/authz/*` for account-builder/billing and
10
+ * are not exposed here.
11
+ */
12
+ export class LicensesService {
13
+ constructor(sdk) {
14
+ this.sdk = sdk;
15
+ }
16
+
17
+ /**
18
+ * Get the tenant's license catalog: license types, seat usage, and the
19
+ * capability catalog.
20
+ * @returns {Promise<Object>} `{ mode, licenseTypes[], capabilities[] }`
21
+ * @example
22
+ * const { licenseTypes } = await sdk.licenses.getCatalog();
23
+ */
24
+ async getCatalog() {
25
+ return internalRequest(this.sdk, '/permissions/licenses', 'GET');
26
+ }
27
+
28
+ /**
29
+ * List users stuck in `needs_seat` (no free seat at assign time).
30
+ * @param {string} [licenseTypeCode] - Filter to one license type
31
+ * @returns {Promise<Object>} `{ results: [{ userId, licenseTypeCode, source, createdAt }] }`
32
+ * @example
33
+ * const { results } = await sdk.licenses.listNeedsSeat('license.support');
34
+ */
35
+ async listNeedsSeat(licenseTypeCode) {
36
+ const params = licenseTypeCode ? { query: { licenseTypeCode } } : {};
37
+ return internalRequest(
38
+ this.sdk,
39
+ '/permissions/licenses/needs-seat',
40
+ 'GET',
41
+ params,
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Get one user's licenses and resolved entitlements.
47
+ * @param {string} userId
48
+ * @returns {Promise<Object>} `{ licenses[], capabilities[], disabledCapabilities[], licenseGate }`
49
+ * @example
50
+ * await sdk.licenses.getUserLicenses('user-123');
51
+ */
52
+ async getUserLicenses(userId) {
53
+ this.sdk.validateParams(
54
+ { userId },
55
+ { userId: { type: 'string', required: true } },
56
+ );
57
+ return internalRequest(
58
+ this.sdk,
59
+ `/permissions/users/${userId}/licenses`,
60
+ 'GET',
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Assign a license directly to a user. 409s with
66
+ * `code:'seat_limit_reached'` at capacity or `code:'license_type_disabled'`
67
+ * for a disabled type.
68
+ * @param {string} userId
69
+ * @param {string} licenseTypeCode
70
+ * @returns {Promise<Object>} The assigned license row
71
+ * @example
72
+ * await sdk.licenses.assignUserLicense('user-123', 'license.core');
73
+ */
74
+ async assignUserLicense(userId, licenseTypeCode) {
75
+ this.sdk.validateParams(
76
+ { userId, licenseTypeCode },
77
+ {
78
+ userId: { type: 'string', required: true },
79
+ licenseTypeCode: { type: 'string', required: true },
80
+ },
81
+ );
82
+ return internalRequest(
83
+ this.sdk,
84
+ `/permissions/users/${userId}/licenses`,
85
+ 'POST',
86
+ { body: { licenseTypeCode } },
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Remove a user's direct license grant (group-sourced grants are
92
+ * unaffected -- see `recomputeUserLicenses`).
93
+ * @param {string} userId
94
+ * @param {string} licenseTypeCode
95
+ * @returns {Promise<Object>}
96
+ * @example
97
+ * await sdk.licenses.removeUserLicense('user-123', 'license.core');
98
+ */
99
+ async removeUserLicense(userId, licenseTypeCode) {
100
+ this.sdk.validateParams(
101
+ { userId, licenseTypeCode },
102
+ {
103
+ userId: { type: 'string', required: true },
104
+ licenseTypeCode: { type: 'string', required: true },
105
+ },
106
+ );
107
+ return internalRequest(
108
+ this.sdk,
109
+ `/permissions/users/${userId}/licenses/${licenseTypeCode}`,
110
+ 'DELETE',
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Get a group's license types (group-sourced grants fan out to members).
116
+ * @param {string} groupId
117
+ * @returns {Promise<Object>} `{ licenseTypeCodes: [] }`
118
+ * @example
119
+ * await sdk.licenses.getGroupLicenses('group-123');
120
+ */
121
+ async getGroupLicenses(groupId) {
122
+ this.sdk.validateParams(
123
+ { groupId },
124
+ { groupId: { type: 'string', required: true } },
125
+ );
126
+ return internalRequest(
127
+ this.sdk,
128
+ `/permissions/groups/${groupId}/licenses`,
129
+ 'GET',
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Replace a group's license types. Triggers a `recomputeUserLicenses`
135
+ * fan-out for every member.
136
+ * @param {string} groupId
137
+ * @param {string[]} licenseTypeCodes
138
+ * @returns {Promise<Object>}
139
+ * @example
140
+ * await sdk.licenses.setGroupLicenses('group-123', ['license.core']);
141
+ */
142
+ async setGroupLicenses(groupId, licenseTypeCodes) {
143
+ this.sdk.validateParams(
144
+ { groupId, licenseTypeCodes },
145
+ {
146
+ groupId: { type: 'string', required: true },
147
+ licenseTypeCodes: { type: 'array', required: true },
148
+ },
149
+ );
150
+ return internalRequest(
151
+ this.sdk,
152
+ `/permissions/groups/${groupId}/licenses`,
153
+ 'PUT',
154
+ { body: { licenseTypeCodes } },
155
+ );
156
+ }
157
+
158
+ /**
159
+ * Set a user's active/disabled status (W1-3). Disabling bumps the
160
+ * user's token_version and logs them out of every queue.
161
+ * @param {string} userId
162
+ * @param {'active'|'disabled'} status
163
+ * @returns {Promise<Object>}
164
+ * @example
165
+ * await sdk.licenses.setUserStatus('user-123', 'disabled');
166
+ */
167
+ async setUserStatus(userId, status) {
168
+ this.sdk.validateParams(
169
+ { userId, status },
170
+ {
171
+ userId: { type: 'string', required: true },
172
+ status: { type: 'string', required: true },
173
+ },
174
+ );
175
+ return internalRequest(
176
+ this.sdk,
177
+ `/permissions/users/${userId}/status`,
178
+ 'PUT',
179
+ { body: { status } },
180
+ );
181
+ }
182
+
183
+ // -- Brand-owner methods (requireBrandOwner; 2D api routes) --------------
184
+
185
+ /**
186
+ * Brand owner: list the tenant accounts under this brand, with their
187
+ * seats and overrides.
188
+ * @returns {Promise<Object>} `{ accounts: [{ id, name, code, aiEnabled, seats[], overrides[] }] }`
189
+ * @example
190
+ * const { accounts } = await sdk.licenses.listBrandAccounts();
191
+ */
192
+ async listBrandAccounts() {
193
+ return internalRequest(this.sdk, '/permissions/brand/accounts', 'GET');
194
+ }
195
+
196
+ /**
197
+ * Brand owner: get one brand account's entitlements.
198
+ * @param {string} accountId
199
+ * @returns {Promise<Object>} `{ id, name, code, aiEnabled, seats[], overrides[] }`
200
+ * @example
201
+ * await sdk.licenses.getBrandAccountEntitlements('acct-123');
202
+ */
203
+ async getBrandAccountEntitlements(accountId) {
204
+ this.sdk.validateParams(
205
+ { accountId },
206
+ { accountId: { type: 'string', required: true } },
207
+ );
208
+ return internalRequest(
209
+ this.sdk,
210
+ `/permissions/brand/accounts/${accountId}/entitlements`,
211
+ 'GET',
212
+ );
213
+ }
214
+
215
+ /**
216
+ * Brand owner: set a brand account's seat pool for one license type.
217
+ * @param {string} accountId
218
+ * @param {string} licenseTypeCode
219
+ * @param {Object} seats
220
+ * @param {number} seats.seatCount
221
+ * @param {boolean} [seats.enabled]
222
+ * @param {boolean} [seats.isDefaultForNewUsers]
223
+ * @returns {Promise<Object>}
224
+ * @example
225
+ * await sdk.licenses.setBrandAccountSeats('acct-123', 'license.core', { seatCount: 25 });
226
+ */
227
+ async setBrandAccountSeats(
228
+ accountId,
229
+ licenseTypeCode,
230
+ { seatCount, enabled, isDefaultForNewUsers } = {},
231
+ ) {
232
+ this.sdk.validateParams(
233
+ { accountId, licenseTypeCode, seatCount },
234
+ {
235
+ accountId: { type: 'string', required: true },
236
+ licenseTypeCode: { type: 'string', required: true },
237
+ seatCount: { type: 'number', required: true },
238
+ },
239
+ );
240
+ const body = { seatCount };
241
+ if (enabled !== undefined) body.enabled = enabled;
242
+ if (isDefaultForNewUsers !== undefined)
243
+ body.isDefaultForNewUsers = isDefaultForNewUsers;
244
+ return internalRequest(
245
+ this.sdk,
246
+ `/permissions/brand/accounts/${accountId}/license-seats/${licenseTypeCode}`,
247
+ 'PUT',
248
+ { body },
249
+ );
250
+ }
251
+
252
+ /**
253
+ * Brand owner: enable or disable a capability override for a brand
254
+ * account.
255
+ * @param {string} accountId
256
+ * @param {string} capabilityCode
257
+ * @param {Object} override
258
+ * @param {'enable'|'disable'} override.mode
259
+ * @param {string} [override.reason]
260
+ * @returns {Promise<Object>}
261
+ * @example
262
+ * await sdk.licenses.setBrandAccountOverride('acct-123', 'capability.chat', { mode: 'disable', reason: 'trial ended' });
263
+ */
264
+ async setBrandAccountOverride(accountId, capabilityCode, { mode, reason } = {}) {
265
+ this.sdk.validateParams(
266
+ { accountId, capabilityCode, mode },
267
+ {
268
+ accountId: { type: 'string', required: true },
269
+ capabilityCode: { type: 'string', required: true },
270
+ mode: { type: 'string', required: true },
271
+ },
272
+ );
273
+ const body = { mode };
274
+ if (reason !== undefined) body.reason = reason;
275
+ return internalRequest(
276
+ this.sdk,
277
+ `/permissions/brand/accounts/${accountId}/capability-overrides/${capabilityCode}`,
278
+ 'PUT',
279
+ { body },
280
+ );
281
+ }
282
+
283
+ /**
284
+ * Brand owner: remove a capability override, reverting to the license
285
+ * template default.
286
+ * @param {string} accountId
287
+ * @param {string} capabilityCode
288
+ * @returns {Promise<Object>}
289
+ * @example
290
+ * await sdk.licenses.deleteBrandAccountOverride('acct-123', 'capability.chat');
291
+ */
292
+ async deleteBrandAccountOverride(accountId, capabilityCode) {
293
+ this.sdk.validateParams(
294
+ { accountId, capabilityCode },
295
+ {
296
+ accountId: { type: 'string', required: true },
297
+ capabilityCode: { type: 'string', required: true },
298
+ },
299
+ );
300
+ return internalRequest(
301
+ this.sdk,
302
+ `/permissions/brand/accounts/${accountId}/capability-overrides/${capabilityCode}`,
303
+ 'DELETE',
304
+ );
305
+ }
306
+
307
+ /**
308
+ * Brand owner: turn the account's AI features on/off.
309
+ * @param {string} accountId
310
+ * @param {boolean} enabled
311
+ * @returns {Promise<Object>}
312
+ * @example
313
+ * await sdk.licenses.setBrandAccountAiEnabled('acct-123', true);
314
+ */
315
+ async setBrandAccountAiEnabled(accountId, enabled) {
316
+ this.sdk.validateParams(
317
+ { accountId, enabled },
318
+ {
319
+ accountId: { type: 'string', required: true },
320
+ enabled: { type: 'boolean', required: true },
321
+ },
322
+ );
323
+ return internalRequest(
324
+ this.sdk,
325
+ `/permissions/brand/accounts/${accountId}/ai-enabled`,
326
+ 'PUT',
327
+ { body: { enabled } },
328
+ );
329
+ }
330
+ }
package/services/login.js CHANGED
@@ -29,14 +29,35 @@ export class LoginService {
29
29
  }
30
30
  }
31
31
 
32
+ // api#222: bearer clients get refreshToken back on /login too (same
33
+ // contract as /login/refresh) -- seed it onto the sdk instance so
34
+ // autoRefresh has something to send on the very first refresh, and
35
+ // also return it for a caller that manages the token itself.
36
+ if (login?.refreshToken) this.sdk.setRefreshToken(login.refreshToken);
37
+
32
38
  return {
33
39
  valid: true,
34
40
  userId: login.userId,
35
41
  namespace: login.namespace,
36
42
  url: login.url,
43
+ refreshToken: login.refreshToken,
37
44
  };
38
45
  }
39
46
 
47
+ // api#222. Cookie clients: no args -- the refreshToken cookie is sent
48
+ // automatically and the new authToken/refreshToken cookies come back on
49
+ // the response, nothing to apply to the SDK instance. Bearer clients:
50
+ // pass the refresh token string; the response's `token` is returned so
51
+ // the caller (or lib/refreshInterceptor.js) can call sdk.setToken(token).
52
+ // Always forceFetch (true): refresh exists to recover a dead session, so
53
+ // it must never ride a socket transport that itself depends on that
54
+ // session still being alive.
55
+ async refresh(refreshToken) {
56
+ const options = {};
57
+ if (refreshToken) options.body = { refreshToken };
58
+ return internalRequest(this.sdk, '/login/refresh', 'POST', options, true);
59
+ }
60
+
40
61
  async logout() {
41
62
  const logout = await internalRequest(this.sdk, '/login', 'DELETE', {}, true);
42
63
 
@@ -1416,44 +1416,4 @@ export class ObjectsService {
1416
1416
  { body: {} },
1417
1417
  );
1418
1418
  }
1419
-
1420
- /**
1421
- * P7 (workflows-v2-plan.md W10/W11/W23) -- runs an immediate full
1422
- * enter+exit sweep for a journey program. Same diff-and-enqueue path a
1423
- * scheduled tick runs, so this can never 429.
1424
- * @param {string} programId
1425
- * @returns {Promise<{queued: true, enterEnqueued: number, exitEnqueued: number}>}
1426
- */
1427
- async runMarketingProgramNow(programId) {
1428
- this.sdk.validateParams(
1429
- { programId },
1430
- { programId: { type: 'string', required: true } },
1431
- );
1432
- return internalRequest(
1433
- this.sdk,
1434
- `/object/marketing-programs/${programId}/run`,
1435
- 'POST',
1436
- { body: {} },
1437
- );
1438
- }
1439
-
1440
- /**
1441
- * P7 -- paginated member list for a journey program (joined to people
1442
- * for display name; never raw peopleId).
1443
- * @param {string} programId
1444
- * @param {{nextId?: string, limit?: number}} [opts]
1445
- * @returns {Promise<{results: object[], pagination: object}>}
1446
- */
1447
- async listMarketingProgramMembers(programId, opts = {}) {
1448
- this.sdk.validateParams(
1449
- { programId },
1450
- { programId: { type: 'string', required: true } },
1451
- );
1452
- return internalRequest(
1453
- this.sdk,
1454
- `/object/marketing-programs/${programId}/members`,
1455
- 'GET',
1456
- { query: opts },
1457
- );
1458
- }
1459
1419
  }
@@ -1,20 +1,31 @@
1
1
  import { internalRequest } from '../base.js';
2
- import { WorkflowToolsService, WorkflowMcpTokensService } from './workflowTools.js';
3
-
4
2
  export class WorkflowsService {
5
3
  constructor(sdk) {
6
4
  this.sdk = sdk;
7
5
  this.items = new WorkflowItemsService(sdk);
8
6
  this.connections = new WorkflowConnectionsService(sdk);
9
7
  this.sessions = new WorkflowSessionsService(sdk);
10
- // P5: MCP tools/tokens surface (workflows-v2-plan.md "REST twin + SDK" row).
11
- this.tools = new WorkflowToolsService(sdk);
12
- this.mcpTokens = new WorkflowMcpTokensService(sdk);
13
8
  }
14
9
 
15
- async listModules({ workflowType } = {}) {
10
+ async getSettings(type) {
11
+ this.sdk.validateParams(
12
+ { type },
13
+ {
14
+ type: { type: 'string', required: true },
15
+ },
16
+ );
17
+
16
18
  const params = {
17
- query: workflowType ? { workflowType } : {},
19
+ query: { type },
20
+ };
21
+
22
+ const result = await internalRequest(this.sdk, '/workflows/settings', 'GET', params);
23
+ return result;
24
+ }
25
+
26
+ async listModules() {
27
+ const params = {
28
+ query: {},
18
29
  };
19
30
 
20
31
  const result = await internalRequest(this.sdk, '/workflows/modules', 'GET', params);
@@ -96,48 +107,6 @@ export class WorkflowsService {
96
107
  );
97
108
  return result;
98
109
  }
99
-
100
- // Delete guard (soft-delete workflows / hard-delete draft versions):
101
- // resolves everything a workflow (or a specific draft version) is
102
- // referenced by, so the client can block the delete with a named list
103
- // instead of a generic FK error. Exactly one of workflowId/
104
- // workflowVersionId is expected.
105
- async references({ workflowId, workflowVersionId } = {}) {
106
- this.sdk.validateParams(
107
- { workflowId, workflowVersionId },
108
- {
109
- workflowId: { type: 'string', required: false },
110
- workflowVersionId: { type: 'string', required: false },
111
- },
112
- );
113
-
114
- const query = {};
115
- if (workflowId) query.workflowId = workflowId;
116
- if (workflowVersionId) query.workflowVersionId = workflowVersionId;
117
-
118
- const result = await internalRequest(this.sdk, '/workflows/references', 'GET', {
119
- query,
120
- });
121
- return result;
122
- }
123
-
124
- // P6 §6 — variable catalogue (inputs/system/context/module outputs) for
125
- // the designer's `{{` autocomplete + variables panel.
126
- async variables(versionId) {
127
- this.sdk.validateParams(
128
- { versionId },
129
- {
130
- versionId: { type: 'string', required: true },
131
- },
132
- );
133
-
134
- const result = await internalRequest(
135
- this.sdk,
136
- `/workflows/${versionId}/variables`,
137
- 'GET',
138
- );
139
- return result;
140
- }
141
110
  }
142
111
 
143
112
  export class WorkflowItemsService {
@@ -434,10 +403,11 @@ export class WorkflowSessionsService {
434
403
  },
435
404
  );
436
405
 
437
- return this.sdk.objects.byId({
438
- object: 'workflowSessions',
439
- id: sessionId,
440
- });
406
+ const result = await internalRequest(this.sdk,
407
+ `/workflows/sessions/${sessionId}`,
408
+ 'GET',
409
+ );
410
+ return result;
441
411
  }
442
412
 
443
413
  async update(sessionId, updateData) {
@@ -461,25 +431,17 @@ export class WorkflowSessionsService {
461
431
  return result;
462
432
  }
463
433
 
464
- /**
465
- * @param {string} sessionId
466
- * @param {string} [reason] - passed through as body.reason -- the
467
- * sessionComplete.js controller already accepts this (defaults to
468
- * 'completed' server-side); P7's exitProgramMember.js passes
469
- * 'programExit'. Backward compatible -- omitting it is unchanged.
470
- */
471
- async complete(sessionId, reason) {
434
+ async complete(sessionId) {
472
435
  this.sdk.validateParams(
473
- { sessionId, reason },
436
+ { sessionId },
474
437
  {
475
438
  sessionId: { type: 'string', required: true },
476
- reason: { type: 'string', required: false },
477
439
  },
478
440
  );
479
441
 
480
- const params = reason ? { body: { reason } } : {};
442
+ const params = {};
481
443
 
482
- const result = await internalRequest(this.sdk,
444
+ const result = await internalRequest(this.sdk,
483
445
  `/workflows/session/${sessionId}/complete`,
484
446
  'PUT',
485
447
  params,
@@ -487,6 +449,21 @@ export class WorkflowSessionsService {
487
449
  return result;
488
450
  }
489
451
 
452
+ async delete(sessionId) {
453
+ this.sdk.validateParams(
454
+ { sessionId },
455
+ {
456
+ sessionId: { type: 'string', required: true },
457
+ },
458
+ );
459
+
460
+ const result = await internalRequest(this.sdk,
461
+ `/workflows/sessions/${sessionId}`,
462
+ 'DELETE',
463
+ );
464
+ return result;
465
+ }
466
+
490
467
  async logTranscript(sessionId, transcriptData) {
491
468
  this.sdk.validateParams(
492
469
  { sessionId },
@@ -536,41 +513,11 @@ export class WorkflowSessionsService {
536
513
  query: { startDate, endDate },
537
514
  };
538
515
 
539
- const result = await internalRequest(this.sdk,
516
+ const result = await internalRequest(this.sdk,
540
517
  `/workflows/${workflowVersionId}/analytics`,
541
518
  'GET',
542
519
  params,
543
520
  );
544
521
  return result;
545
522
  }
546
-
547
- // P10 click-to-filter: GET /workflows/:workflowVersionId/sessions ->
548
- // {sessionIds}. filter is {workflowItemId?, fromItemId?, toItemId?,
549
- // startDate?, endDate?} -- workflowItemId alone, or fromItemId+toItemId
550
- // together, is required (validated server-side in sessionsByPath.js).
551
- async sessionsByPath(workflowVersionId, filter = {}) {
552
- this.sdk.validateParams(
553
- { workflowVersionId },
554
- {
555
- workflowVersionId: { type: 'string', required: true },
556
- },
557
- );
558
-
559
- const { workflowItemId, fromItemId, toItemId, startDate, endDate } =
560
- filter || {};
561
- const query = {};
562
- if (workflowItemId) query.workflowItemId = workflowItemId;
563
- if (fromItemId) query.fromItemId = fromItemId;
564
- if (toItemId) query.toItemId = toItemId;
565
- if (startDate) query.startDate = startDate;
566
- if (endDate) query.endDate = endDate;
567
-
568
- const result = await internalRequest(
569
- this.sdk,
570
- `/workflows/${workflowVersionId}/sessions`,
571
- 'GET',
572
- { query },
573
- );
574
- return result;
575
- }
576
523
  }
@@ -1,74 +0,0 @@
1
- import { internalRequest } from '../base.js';
2
-
3
- // P5 "REST twin + SDK" / "Auth" rows: workflows.tools.{list,call} +
4
- // workflows.mcpTokens.{create,list,revoke}. Kept in its own file (repo
5
- // file-size convention) rather than growing workflows.js further.
6
-
7
- export class WorkflowToolsService {
8
- constructor(sdk) {
9
- this.sdk = sdk;
10
- }
11
-
12
- // GET /workflows/tools -> [{name, description, inputSchema, outputSchema, annotations}]
13
- async list() {
14
- const result = await internalRequest(this.sdk, '/workflows/tools', 'GET');
15
- return result;
16
- }
17
-
18
- // POST /workflows/tools/:slug/call -> {completed, outputs, isError, message, sessionId}
19
- async call(slug, input) {
20
- this.sdk.validateParams(
21
- { slug },
22
- { slug: { type: 'string', required: true } },
23
- );
24
-
25
- const params = { body: { input: input || {} } };
26
- const result = await internalRequest(
27
- this.sdk,
28
- `/workflows/tools/${slug}/call`,
29
- 'POST',
30
- params,
31
- );
32
- return result;
33
- }
34
- }
35
-
36
- export class WorkflowMcpTokensService {
37
- constructor(sdk) {
38
- this.sdk = sdk;
39
- }
40
-
41
- // POST /workflows/mcp/tokens -> {id, token, name} -- `token` (the raw
42
- // JWT) is shown exactly once, never retrievable again.
43
- async create({ name } = {}) {
44
- const params = { body: name ? { name } : {} };
45
- const result = await internalRequest(
46
- this.sdk,
47
- '/workflows/mcp/tokens',
48
- 'POST',
49
- params,
50
- );
51
- return result;
52
- }
53
-
54
- // GET /workflows/mcp/tokens -> {tokens: [{id, name, createdAt, lastUsedAt, revokedAt}]}
55
- async list() {
56
- const result = await internalRequest(this.sdk, '/workflows/mcp/tokens', 'GET');
57
- return result;
58
- }
59
-
60
- // DELETE /workflows/mcp/tokens/:id -> {id, revoked:true}
61
- async revoke(id) {
62
- this.sdk.validateParams(
63
- { id },
64
- { id: { type: 'string', required: true } },
65
- );
66
-
67
- const result = await internalRequest(
68
- this.sdk,
69
- `/workflows/mcp/tokens/${id}`,
70
- 'DELETE',
71
- );
72
- return result;
73
- }
74
- }