quickbase-js 0.1.0-beta.8 → 0.1.0-beta.9

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +46 -130
  3. package/dist/esm/quickbase.d.ts +822 -119
  4. package/dist/esm/quickbase.js +373 -236
  5. package/dist/esm/quickbase.js.map +1 -1
  6. package/dist/esm/quickbase.min.js +1 -1
  7. package/dist/esm/quickbase.min.js.map +1 -1
  8. package/dist/umd/quickbase.umd.js +373 -236
  9. package/dist/umd/quickbase.umd.js.map +1 -1
  10. package/dist/umd/quickbase.umd.min.js +1 -1
  11. package/dist/umd/quickbase.umd.min.js.map +1 -1
  12. package/open-api/generate-unified-interface.ts +97 -17
  13. package/open-api/utils/generateJsDoc.ts +96 -0
  14. package/package.json +3 -1
  15. package/src/ThrottleBucket.ts +17 -12
  16. package/src/authorizationStrategy.ts +246 -0
  17. package/src/generated-unified/QuickbaseClient.ts +809 -119
  18. package/src/invokeMethod.ts +150 -265
  19. package/src/quickbaseClient.ts +85 -58
  20. package/src/rateLimiter.ts +25 -0
  21. package/src/tokenCache.ts +10 -26
  22. package/tests/vitest/qb/auth/mockQuickbaseServer.ts +60 -0
  23. package/tests/vitest/qb/auth/retryUserToken401.test.ts +59 -42
  24. package/tests/vitest/qb/auth/ssoTokenIntegrationWithServer.test.ts +65 -0
  25. package/tests/vitest/qb/fields/getFieldUsage.test.ts +58 -150
  26. package/tests/vitest/qb/formulas/runFormula.test.ts +96 -0
  27. package/tests/vitest/qb/tables/createRelationship.test.ts +102 -0
  28. package/tests/vitest/qb/tables/deleteRelationship.test.ts +143 -0
  29. package/tests/vitest/qb/tables/updateRelationship.test.ts +118 -145
  30. package/tests/vitest/unit/ThrottleBucket.test.ts +95 -0
  31. package/tests/vitest/unit/apps/copyApp.test.ts +133 -40
  32. package/tests/vitest/unit/apps/updateApp.test.ts +42 -11
  33. package/tests/vitest/unit/auth/fetchTempToken401.test.ts +3 -2
  34. package/tests/vitest/unit/auth/getTempToken.test.ts +42 -18
  35. package/tests/vitest/unit/auth/retryTempToken401.test.ts +46 -64
  36. package/tests/vitest/unit/auth/retryTempTokenMaxRetries.test.ts +235 -0
  37. package/tests/vitest/unit/auth/retryUserToken401.test.ts +56 -43
  38. package/tests/vitest/unit/auth/ssoTokenRefresh.test.ts +325 -0
  39. package/tests/vitest/unit/fields/createField.test.ts +180 -116
  40. package/tests/vitest/unit/fields/getFieldUsage.test.ts +17 -3
  41. package/tests/vitest/unit/fields/updateField.test.ts +12 -7
  42. package/tests/vitest/unit/formulas/runFormula.test.ts +205 -0
  43. package/tests/vitest/unit/index.ts +2 -1
  44. package/tests/vitest/unit/inferHttpMethod.test.ts +131 -0
  45. package/tests/vitest/unit/rateLimit.test.ts +4 -2
  46. package/tests/vitest/unit/tables/createRelationship.test.ts +212 -0
  47. package/tests/vitest/unit/tables/deleteRelationship.test.ts +97 -0
  48. package/tests/vitest/unit/tables/updateRelationship.test.ts +12 -10
  49. package/tests/vitest/unit/tokenLifespan.test.ts +149 -83
  50. package/tools/log-gen.ts +14 -5
  51. package/tools/log-gen.yaml +710 -268
  52. package/tests/vitest/unit/auth/retryTempTokenTwo401.test.ts +0 -81
@@ -16,11 +16,15 @@ console.log("Path modules imported");
16
16
  import { fileURLToPath } from "url";
17
17
  console.log("URL module imported");
18
18
 
19
- import { OpenAPIV2 } from "openapi-types"; // Use module name, not relative path
19
+ import { OpenAPIV2 } from "openapi-types";
20
20
  console.log("openapi-types imported");
21
21
 
22
- // Temporary simplifyName fallback
23
- const simplifyName = (str: string) => str;
22
+ import { Project } from "ts-morph";
23
+ console.log("ts-morph imported");
24
+
25
+ import { generateJsDoc } from "./utils/generateJsDoc.js";
26
+
27
+ const simplifyName = (str: string): string => str;
24
28
 
25
29
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
30
  console.log("__dirname set:", __dirname);
@@ -110,7 +114,57 @@ function mapRefToType(
110
114
  return "any";
111
115
  }
112
116
 
113
- function generateInterface() {
117
+ interface PropertyDetail {
118
+ name: string;
119
+ type: string;
120
+ required: boolean;
121
+ jsdoc?: string;
122
+ }
123
+
124
+ function parseInterfaceProperties(
125
+ modelName: string,
126
+ modelsDir: string
127
+ ): PropertyDetail[] {
128
+ const project = new Project();
129
+ const filePath = join(modelsDir, `${modelName}.ts`);
130
+ if (!existsSync(filePath)) {
131
+ console.warn(`Model file ${filePath} not found for ${modelName}`);
132
+ return [];
133
+ }
134
+
135
+ const sourceFile = project.addSourceFileAtPath(filePath);
136
+ const interfaceDec = sourceFile.getInterface(modelName);
137
+ if (!interfaceDec) {
138
+ console.warn(`Interface ${modelName} not found in ${filePath}`);
139
+ return [];
140
+ }
141
+
142
+ return interfaceDec.getProperties().map((prop) => {
143
+ const jsDocs = prop.getJsDocs();
144
+ const jsdocText =
145
+ jsDocs.length > 0 ? jsDocs[0].getDescription().trim() : undefined;
146
+ return {
147
+ name: prop.getName(),
148
+ type: prop.getType().getText(prop),
149
+ required: !prop.hasQuestionToken(),
150
+ jsdoc: jsdocText,
151
+ };
152
+ });
153
+ }
154
+
155
+ interface ParamDetail {
156
+ name: string;
157
+ type: string;
158
+ required: boolean;
159
+ properties: PropertyDetail[];
160
+ }
161
+
162
+ function generateInterface(includeResponseProperties: boolean = false): void {
163
+ console.log(
164
+ "Generating interface with includeResponseProperties:",
165
+ includeResponseProperties
166
+ );
167
+
114
168
  console.log("Checking spec file:", SPEC_FILE);
115
169
  if (!existsSync(SPEC_FILE)) {
116
170
  console.error(
@@ -121,7 +175,7 @@ function generateInterface() {
121
175
 
122
176
  console.log("Reading spec file...");
123
177
  const specContent = readFileSync(SPEC_FILE, "utf8");
124
- const spec = JSON.parse(specContent) as OpenAPIV2.Document;
178
+ const spec: OpenAPIV2.Document = JSON.parse(specContent);
125
179
  console.log("Spec parsed, keys:", Object.keys(spec));
126
180
 
127
181
  const { paths } = spec;
@@ -158,7 +212,9 @@ function generateInterface() {
158
212
 
159
213
  const opId = simplifyName(op.operationId);
160
214
  const summary = op.summary || "No description.";
161
- const params = (op.parameters || [])
215
+ const docLink = `https://developer.quickbase.com/operation/${op.operationId}`;
216
+
217
+ const paramDetails: ParamDetail[] = (op.parameters || [])
162
218
  .filter((p: OpenAPIV2.ParameterObject | OpenAPIV2.ReferenceObject) => {
163
219
  const param = p as OpenAPIV2.ParameterObject;
164
220
  return !["QB-Realm-Hostname", "Authorization", "User-Agent"].includes(
@@ -167,8 +223,9 @@ function generateInterface() {
167
223
  })
168
224
  .map((param: OpenAPIV2.ParameterObject | OpenAPIV2.ReferenceObject) => {
169
225
  const p = param as OpenAPIV2.ParameterObject;
170
- if (!p.name) return "";
226
+ if (!p.name) return null;
171
227
  let type = "any";
228
+ let properties: PropertyDetail[] = [];
172
229
  if ("schema" in p && p.schema) {
173
230
  type = mapRefToType(
174
231
  p.schema,
@@ -178,13 +235,28 @@ function generateInterface() {
178
235
  availableModels,
179
236
  missingTypes
180
237
  );
238
+ if ("$ref" in p.schema && p.schema.$ref) {
239
+ const refParts = p.schema.$ref.split("/");
240
+ const model = refParts[refParts.length - 1];
241
+ const pascalModel =
242
+ model.charAt(0).toUpperCase() + model.slice(1);
243
+ properties = parseInterfaceProperties(pascalModel, MODELS_DIR);
244
+ }
181
245
  } else if ("type" in p && p.type) {
182
246
  type = mapOpenApiTypeToTs(p.type);
183
247
  }
184
248
  const paramName = p.in === "body" ? "body" : p.name;
185
- return `${paramName}${p.required ? "" : "?"}: ${type}`;
249
+ return {
250
+ name: paramName,
251
+ type,
252
+ required: p.required || false,
253
+ properties,
254
+ };
186
255
  })
187
- .filter((param) => param !== "")
256
+ .filter((p): p is ParamDetail => p !== null);
257
+
258
+ const params = paramDetails
259
+ .map((p) => `${p.name}${p.required ? "" : "?"}: ${p.type}`)
188
260
  .join("; ");
189
261
 
190
262
  const returnTypes = ["200", "207"]
@@ -207,13 +279,21 @@ function generateInterface() {
207
279
  ? returnTypes.join(" | ")
208
280
  : returnTypes[0] || "void";
209
281
 
210
- const jsDoc = [
211
- ` /**`,
212
- ` * ${summary}`,
213
- ` * @param params - Parameters for ${opId}`,
214
- ` * @returns Promise resolving to ${opId} response`,
215
- ` */`,
216
- ].join("\n");
282
+ const returnTypeDetails: PropertyDetail[] =
283
+ includeResponseProperties &&
284
+ returnTypes.length === 1 &&
285
+ returnTypes[0] !== "void"
286
+ ? parseInterfaceProperties(returnTypes[0], MODELS_DIR)
287
+ : [];
288
+
289
+ const jsDoc = generateJsDoc({
290
+ summary,
291
+ opId,
292
+ paramDetails,
293
+ returnType,
294
+ returnTypeDetails,
295
+ docLink,
296
+ });
217
297
 
218
298
  methods.push(
219
299
  `${jsDoc}\n ${opId}: (params: { ${params} }) => Promise<${returnType}>;`
@@ -247,7 +327,7 @@ function generateInterface() {
247
327
 
248
328
  console.log("Entering try block");
249
329
  try {
250
- generateInterface();
330
+ generateInterface(true); // Toggle response properties here
251
331
  } catch (error) {
252
332
  console.error("Error in generateInterface:", error);
253
333
  process.exit(1);
@@ -0,0 +1,96 @@
1
+ interface ParamDetail {
2
+ name: string;
3
+ type: string;
4
+ required: boolean;
5
+ properties: {
6
+ name: string;
7
+ type: string;
8
+ required: boolean;
9
+ jsdoc?: string;
10
+ }[];
11
+ }
12
+
13
+ interface JsDocOptions {
14
+ summary: string;
15
+ opId: string;
16
+ paramDetails: ParamDetail[];
17
+ returnType: string;
18
+ returnTypeDetails: {
19
+ name: string;
20
+ type: string;
21
+ required: boolean;
22
+ jsdoc?: string;
23
+ }[];
24
+ docLink: string;
25
+ }
26
+
27
+ export function generateJsDoc({
28
+ summary,
29
+ opId,
30
+ paramDetails,
31
+ returnType,
32
+ returnTypeDetails,
33
+ docLink,
34
+ }: JsDocOptions): string {
35
+ const jsDocLines: string[] = [
36
+ ` /**`,
37
+ ` * ${summary}`,
38
+ ` *`,
39
+ ` * @param {Object} params _Object containing the parameters for_ ${opId}`,
40
+ ];
41
+
42
+ // Parameters section
43
+ if (paramDetails.length > 0) {
44
+ paramDetails.forEach((p) => {
45
+ jsDocLines.push(
46
+ ` * @param {${p.type}} params.${p.name} _${
47
+ p.required ? "Required" : "Optional"
48
+ } parameter with properties_`
49
+ );
50
+ if (p.properties.length > 0) {
51
+ p.properties.forEach((prop) => {
52
+ const propDesc = prop.jsdoc
53
+ ? prop.jsdoc.replace(/@type\s*{[^}]+}\s*@memberof\s*\w+/, "").trim()
54
+ : `Type: ${prop.type}`;
55
+ jsDocLines.push(
56
+ ` * - **${prop.name}** (\`${prop.type}\`${
57
+ prop.required ? ", required" : ", optional"
58
+ }) _${propDesc}_`
59
+ );
60
+ });
61
+ }
62
+ });
63
+ } else {
64
+ jsDocLines.push(` * No parameters`);
65
+ }
66
+
67
+ // Returns section
68
+ jsDocLines.push(` *`);
69
+ if (returnTypeDetails.length > 0) {
70
+ jsDocLines.push(
71
+ ` * @returns {Promise<${returnType}>} _Promise resolving to the ${opId} response with properties_`
72
+ );
73
+ returnTypeDetails.forEach((prop) => {
74
+ const propDesc = prop.jsdoc
75
+ ? prop.jsdoc.replace(/@type\s*{[^}]+}\s*@memberof\s*\w+/, "").trim()
76
+ : `Type: ${prop.type}`;
77
+ jsDocLines.push(
78
+ ` * - **${prop.name}** (\`${prop.type}\`${
79
+ prop.required ? ", required" : ", optional"
80
+ }) _${propDesc}_`
81
+ );
82
+ });
83
+ } else {
84
+ jsDocLines.push(
85
+ ` * @returns {Promise<${returnType}>} _Promise resolving to the ${opId} response_`
86
+ );
87
+ }
88
+
89
+ jsDocLines.push(
90
+ ` *`,
91
+ ` * @see {@link ${docLink}} Official Quickbase API documentation`
92
+ );
93
+ jsDocLines.push(` */`);
94
+
95
+ return jsDocLines.join("\n");
96
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quickbase-js",
3
- "version": "0.1.0-beta.8",
3
+ "version": "0.1.0-beta.9",
4
4
  "description": "QuickBase JavaScript client for frameworks and vanilla JS",
5
5
  "type": "module",
6
6
  "main": "dist/esm/quickbase.js",
@@ -52,6 +52,7 @@
52
52
  "test:vitest:unit:apps": "vitest run tests/vitest/unit/apps/*.test.ts",
53
53
  "test:vitest:unit:auth": "vitest run tests/vitest/unit/auth/*.test.ts",
54
54
  "test:vitest:unit:fields": "vitest run tests/vitest/unit/fields/*.test.ts",
55
+ "test:vitest:unit:formulas": "vitest run tests/vitest/unit/formulas/*.test.ts",
55
56
  "test:vitest:unit:records": "vitest run tests/vitest/unit/records/*.test.ts",
56
57
  "test:vitest:unit:tables": "vitest run tests/vitest/unit/tables/*.test.ts",
57
58
  "test:pw:qb:auth": "playwright test tests/playwright/qb/auth"
@@ -67,6 +68,7 @@
67
68
  "@types/js-yaml": "^4.0.9",
68
69
  "@types/node": "^22.13.10",
69
70
  "dotenv": "^16.4.7",
71
+ "express": "^4.21.2",
70
72
  "fast-json-patch": "^3.1.1",
71
73
  "glob": "^10.4.5",
72
74
  "js-yaml": "^4.1.0",
@@ -1,18 +1,18 @@
1
- // src/TokenBucket.ts
1
+ // src/ThrottleBucket.ts
2
2
  export class ThrottleBucket {
3
3
  private tokens: number;
4
4
  private maxTokens: number;
5
5
  private refillRate: number; // Tokens per second
6
6
  private lastRefill: number;
7
+ private pending: Promise<void> = Promise.resolve(); // Queue for sequential execution
7
8
 
8
9
  constructor(rate: number, burst: number) {
9
- this.tokens = burst; // Start with full burst capacity
10
+ this.tokens = burst;
10
11
  this.maxTokens = burst;
11
12
  this.refillRate = rate;
12
13
  this.lastRefill = Date.now();
13
14
  }
14
15
 
15
- // Refill tokens based on elapsed time
16
16
  private refill(): void {
17
17
  const now = Date.now();
18
18
  const elapsed = (now - this.lastRefill) / 1000; // Seconds elapsed
@@ -21,16 +21,21 @@ export class ThrottleBucket {
21
21
  this.lastRefill = now;
22
22
  }
23
23
 
24
- // Acquire a token, waiting if necessary
25
24
  async acquire(): Promise<void> {
26
- this.refill();
27
- if (this.tokens >= 1) {
25
+ // Chain the new acquisition onto the pending queue
26
+ const previous = this.pending;
27
+ this.pending = (async () => {
28
+ await previous; // Wait for prior calls to complete
29
+ this.refill();
30
+ if (this.tokens >= 1) {
31
+ this.tokens -= 1;
32
+ return;
33
+ }
34
+ const waitTime = ((1 - this.tokens) / this.refillRate) * 1000; // ms until next token
35
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
36
+ this.refill();
28
37
  this.tokens -= 1;
29
- return;
30
- }
31
- const waitTime = ((1 - this.tokens) / this.refillRate) * 1000; // ms until next token
32
- await new Promise((resolve) => setTimeout(resolve, waitTime));
33
- this.refill();
34
- this.tokens -= 1;
38
+ })();
39
+ await this.pending;
35
40
  }
36
41
  }
@@ -0,0 +1,246 @@
1
+ // src/authorizationStrategy.ts
2
+
3
+ import { TokenCache } from "./tokenCache";
4
+
5
+ export interface AuthorizationStrategy {
6
+ getToken(dbid: string): Promise<string | undefined>;
7
+ applyHeaders(headers: Record<string, string>, token: string): void;
8
+ handleError(
9
+ status: number,
10
+ params: any,
11
+ fetchTempToken: (dbid: string) => Promise<string>,
12
+ attempt: number,
13
+ maxAttempts: number,
14
+ debug?: boolean,
15
+ methodName?: string
16
+ ): Promise<string | null>;
17
+ }
18
+
19
+ export class TempTokenStrategy implements AuthorizationStrategy {
20
+ constructor(
21
+ private tokenCache: TokenCache,
22
+ private initialTempToken?: string
23
+ ) {}
24
+
25
+ async getToken(dbid: string): Promise<string | undefined> {
26
+ return this.tokenCache.get(dbid) || this.initialTempToken;
27
+ }
28
+
29
+ applyHeaders(headers: Record<string, string>, token: string): void {
30
+ headers["Authorization"] = `QB-TEMP-TOKEN ${token}`;
31
+ }
32
+
33
+ async handleError(
34
+ status: number,
35
+ params: any,
36
+ fetchTempToken: (dbid: string) => Promise<string>,
37
+ attempt: number,
38
+ maxAttempts: number,
39
+ debug?: boolean,
40
+ methodName?: string
41
+ ): Promise<string | null> {
42
+ if (status !== 401 || attempt >= maxAttempts - 1) return null;
43
+ if (debug)
44
+ console.log(
45
+ `Authorization error for ${
46
+ methodName || "method"
47
+ } (temp token), refreshing token:`
48
+ );
49
+ const dbid = extractDbid(params);
50
+ if (!dbid) {
51
+ if (debug)
52
+ console.log(
53
+ `No dbid available for ${
54
+ methodName || "method"
55
+ }, skipping token refresh`
56
+ );
57
+ return null;
58
+ }
59
+ if (debug) console.log(`Refreshing temp token for dbid: ${dbid}`);
60
+ const newToken = await fetchTempToken(dbid);
61
+ this.tokenCache.set(dbid, newToken);
62
+ return newToken;
63
+ }
64
+ }
65
+
66
+ export class UserTokenStrategy implements AuthorizationStrategy {
67
+ constructor(private userToken: string) {}
68
+
69
+ async getToken(_dbid: string): Promise<string> {
70
+ return this.userToken;
71
+ }
72
+
73
+ applyHeaders(headers: Record<string, string>, token: string): void {
74
+ headers["Authorization"] = `QB-USER-TOKEN ${token}`;
75
+ }
76
+
77
+ async handleError(
78
+ status: number,
79
+ _params: any,
80
+ _fetchTempToken: (dbid: string) => Promise<string>,
81
+ attempt: number,
82
+ maxAttempts: number,
83
+ debug?: boolean,
84
+ methodName?: string
85
+ ): Promise<string | null> {
86
+ if (status !== 401 || attempt >= maxAttempts - 1) return null;
87
+ if (debug)
88
+ console.log(
89
+ `Retrying ${
90
+ methodName || "method"
91
+ } with existing user token: ${this.userToken.substring(0, 10)}...`
92
+ );
93
+ return this.userToken;
94
+ }
95
+ }
96
+
97
+ export class SsoTokenStrategy implements AuthorizationStrategy {
98
+ private currentToken: string | undefined;
99
+
100
+ constructor(
101
+ private samlToken: string,
102
+ private realm: string,
103
+ private fetchApi: typeof fetch,
104
+ private debug: boolean = false
105
+ ) {}
106
+
107
+ async getToken(_dbid: string): Promise<string | undefined> {
108
+ if (!this.currentToken) {
109
+ this.currentToken = await this.fetchSsoToken({
110
+ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
111
+ requested_token_type:
112
+ "urn:quickbase:params:oauth:token-type:temp_token",
113
+ subject_token: this.samlToken,
114
+ subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
115
+ });
116
+ }
117
+ return this.currentToken;
118
+ }
119
+
120
+ applyHeaders(headers: Record<string, string>, token: string): void {
121
+ headers["Authorization"] = `QB-TEMP-TOKEN ${token}`;
122
+ }
123
+
124
+ async handleError(
125
+ status: number,
126
+ _params: any,
127
+ _fetchTempToken: (dbid: string) => Promise<string>,
128
+ attempt: number,
129
+ maxAttempts: number,
130
+ debug?: boolean,
131
+ methodName?: string
132
+ ): Promise<string | null> {
133
+ if (status !== 401 || attempt >= maxAttempts - 1) return null;
134
+
135
+ if (debug || this.debug) {
136
+ console.log(
137
+ `Authorization error for ${
138
+ methodName || "method"
139
+ } (SSO), refreshing token`
140
+ );
141
+ }
142
+
143
+ const newToken = await this.refreshSsoToken(debug || this.debug);
144
+ if (newToken) {
145
+ this.currentToken = newToken;
146
+ return newToken;
147
+ }
148
+ return null;
149
+ }
150
+
151
+ private async refreshSsoToken(debug: boolean = false): Promise<string> {
152
+ const payload = {
153
+ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
154
+ requested_token_type: "urn:quickbase:params:oauth:token-type:temp_token",
155
+ subject_token: this.samlToken,
156
+ subject_token_type: "urn:ietf:params:oauth:token-type:saml2",
157
+ };
158
+
159
+ const response = await this.fetchApi(
160
+ `https://api.quickbase.com/v1/auth/oauth/token`,
161
+ {
162
+ method: "POST",
163
+ headers: {
164
+ "QB-Realm-Hostname": `${this.realm}.quickbase.com`,
165
+ "Content-Type": "application/json",
166
+ },
167
+ body: JSON.stringify(payload),
168
+ credentials: "omit",
169
+ }
170
+ );
171
+
172
+ if (!response.ok) {
173
+ const errorBody = await response.json().catch(() => ({}));
174
+ if (debug) {
175
+ console.log(`[SSO Refresh] Failed: ${response.status}`, errorBody);
176
+ }
177
+ throw new Error(
178
+ `SSO token refresh failed: ${errorBody.message || "Unknown error"}`
179
+ );
180
+ }
181
+
182
+ const result = await response.json();
183
+ const newToken = result.access_token;
184
+ if (!newToken) {
185
+ throw new Error("No access token returned from SSO token exchange");
186
+ }
187
+
188
+ if (debug) {
189
+ console.log(`[SSO Refresh] New token: ${newToken.substring(0, 10)}...`);
190
+ }
191
+ return newToken;
192
+ }
193
+
194
+ private async fetchSsoToken(params: {
195
+ grant_type: string;
196
+ requested_token_type: string;
197
+ subject_token: string;
198
+ subject_token_type: string;
199
+ }): Promise<string> {
200
+ const response = await this.fetchApi(
201
+ `https://api.quickbase.com/v1/auth/oauth/token`,
202
+ {
203
+ method: "POST",
204
+ headers: {
205
+ "QB-Realm-Hostname": `${this.realm}.quickbase.com`,
206
+ "Content-Type": "application/json",
207
+ },
208
+ body: JSON.stringify(params),
209
+ credentials: "omit",
210
+ }
211
+ );
212
+
213
+ if (!response.ok) {
214
+ const errorBody = await response.json().catch(() => ({}));
215
+ if (this.debug) {
216
+ console.log(`[fetchSsoToken] Failed: ${response.status}`, errorBody);
217
+ }
218
+ throw new Error(
219
+ `SSO token fetch failed: ${errorBody.message || "Unknown error"}`
220
+ );
221
+ }
222
+
223
+ const result = await response.json();
224
+ const newToken = result.access_token;
225
+ if (!newToken) {
226
+ throw new Error("No access token returned from SSO token exchange");
227
+ }
228
+
229
+ if (this.debug) {
230
+ console.log(
231
+ `[fetchSsoToken] Fetched token: ${newToken.substring(0, 10)}...`
232
+ );
233
+ }
234
+ return newToken;
235
+ }
236
+ }
237
+
238
+ export function extractDbid(params: any): string | undefined {
239
+ return (
240
+ params.dbid ||
241
+ params.tableId ||
242
+ params.appId ||
243
+ params.body?.from ||
244
+ params.body?.to
245
+ );
246
+ }