quickbase-js 2.4.0 → 2.4.1

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 (46) hide show
  1. package/README.md +144 -76
  2. package/dist/quickbase.esm.js +3 -3
  3. package/dist/quickbase.min.js +3 -3
  4. package/dist/quickbase.min.js.map +1 -1
  5. package/package.json +13 -9
  6. package/src/auth/index.ts +55 -0
  7. package/src/auth/sso-token.ts +143 -0
  8. package/src/auth/temp-token.ts +160 -0
  9. package/src/auth/ticket.ts +329 -0
  10. package/src/auth/token-cache.ts +97 -0
  11. package/src/auth/types.ts +50 -0
  12. package/src/auth/user-token.ts +38 -0
  13. package/src/cli/schema.ts +512 -0
  14. package/src/client/index.ts +36 -0
  15. package/src/client/pagination.ts +337 -0
  16. package/src/client/request.ts +544 -0
  17. package/src/client/retry.ts +169 -0
  18. package/src/client/throttle.ts +105 -0
  19. package/src/core/config.ts +160 -0
  20. package/src/core/dates.ts +63 -0
  21. package/src/core/errors.ts +349 -0
  22. package/src/core/index.ts +50 -0
  23. package/src/core/logger.ts +113 -0
  24. package/src/core/schema.ts +213 -0
  25. package/src/core/transform.ts +295 -0
  26. package/src/core/types.ts +337 -0
  27. package/src/generated/client.ts +1785 -0
  28. package/src/generated/operations.json +672 -0
  29. package/src/generated/types.ts +4884 -0
  30. package/src/index.ts +259 -0
  31. package/src/xml/codepages.ts +119 -0
  32. package/src/xml/dbvars.ts +75 -0
  33. package/src/xml/errors.ts +133 -0
  34. package/src/xml/fields.ts +121 -0
  35. package/src/xml/groups.ts +560 -0
  36. package/src/xml/html.ts +152 -0
  37. package/src/xml/index.ts +1196 -0
  38. package/src/xml/metadata.ts +111 -0
  39. package/src/xml/records.ts +301 -0
  40. package/src/xml/request.ts +238 -0
  41. package/src/xml/roles.ts +321 -0
  42. package/src/xml/schema.ts +436 -0
  43. package/src/xml/transform.ts +381 -0
  44. package/src/xml/types.ts +602 -0
  45. package/src/xml/users.ts +221 -0
  46. package/src/xml/webhooks.ts +260 -0
package/package.json CHANGED
@@ -1,24 +1,25 @@
1
1
  {
2
2
  "name": "quickbase-js",
3
- "version": "2.4.0",
4
- "description": "TypeScript/JavaScript client for the QuickBase JSON RESTful API",
3
+ "version": "2.4.1",
4
+ "description": "TypeScript client for the QuickBase JSON and XML APIs: every endpoint, pagination, retries, code-page temp tokens",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
8
- "browser": "dist/quickbase.min.js",
9
8
  "types": "dist/index.d.ts",
10
9
  "exports": {
11
10
  ".": {
12
- "import": "./dist/index.js",
13
11
  "types": "./dist/index.d.ts",
14
- "browser": "./dist/quickbase.min.js"
15
- }
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
16
  },
17
17
  "bin": {
18
18
  "quickbase-js": "./bin/quickbase-js.js"
19
19
  },
20
20
  "files": [
21
21
  "dist",
22
+ "src",
22
23
  "bin",
23
24
  "README.md",
24
25
  "LICENSE"
@@ -41,7 +42,7 @@
41
42
  "spec:validate": "tsx spec/tools/cli.ts validate",
42
43
  "spec:generate": "tsx scripts/spec/generate.ts",
43
44
  "spec:build": "npm run spec:normalize && npm run spec:patch && npm run spec:validate && npm run spec:generate",
44
- "docs": "typedoc",
45
+ "docs": "typedoc && node scripts/build-llms.js",
45
46
  "docs:watch": "typedoc --watch",
46
47
  "prepublishOnly": "npm run clean && npm run build && npm run test"
47
48
  },
@@ -57,7 +58,7 @@
57
58
  "license": "MIT",
58
59
  "repository": {
59
60
  "type": "git",
60
- "url": "https://github.com/DrewBradfordXYZ/quickbase-js.git"
61
+ "url": "git+https://github.com/DrewBradfordXYZ/quickbase-js.git"
61
62
  },
62
63
  "bugs": {
63
64
  "url": "https://github.com/DrewBradfordXYZ/quickbase-js/issues"
@@ -75,5 +76,8 @@
75
76
  "typescript": "^5.4.0",
76
77
  "vitest": "^1.4.0",
77
78
  "yaml": "^2.4.0"
78
- }
79
+ },
80
+ "unpkg": "dist/quickbase.min.js",
81
+ "jsdelivr": "dist/quickbase.min.js",
82
+ "sideEffects": false
79
83
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Authentication module exports
3
+ */
4
+
5
+ export type { AuthStrategy, AuthContext, CachedToken } from './types.js';
6
+ export { UserTokenStrategy } from './user-token.js';
7
+ export { TempTokenStrategy } from './temp-token.js';
8
+ export { SsoTokenStrategy } from './sso-token.js';
9
+ export { TicketStrategy } from './ticket.js'; // XML-API-TICKET: Remove if XML API discontinued
10
+ export { TokenCache } from './token-cache.js';
11
+
12
+ import type { AuthConfig } from '../core/types.js';
13
+ import type { AuthStrategy, AuthContext } from './types.js';
14
+ import { UserTokenStrategy } from './user-token.js';
15
+ import { TempTokenStrategy } from './temp-token.js';
16
+ import { SsoTokenStrategy } from './sso-token.js';
17
+ import { TicketStrategy } from './ticket.js'; // XML-API-TICKET: Remove if XML API discontinued
18
+
19
+ /**
20
+ * Factory function to create the appropriate auth strategy
21
+ */
22
+ export function createAuthStrategy(
23
+ authConfig: AuthConfig,
24
+ context: AuthContext
25
+ ): AuthStrategy {
26
+ switch (authConfig.type) {
27
+ case 'user-token':
28
+ return new UserTokenStrategy(authConfig.userToken, context);
29
+
30
+ case 'temp-token':
31
+ return new TempTokenStrategy(authConfig.userToken, context, {
32
+ appToken: authConfig.appToken,
33
+ tokenLifespanMs: authConfig.tokenLifespanMs,
34
+ });
35
+
36
+ case 'sso':
37
+ return new SsoTokenStrategy(authConfig.samlToken, context);
38
+
39
+ // XML-API-TICKET: Remove this case if XML API is discontinued
40
+ case 'ticket':
41
+ return new TicketStrategy(
42
+ authConfig.username,
43
+ authConfig.password,
44
+ {
45
+ hours: authConfig.hours,
46
+ onExpired: authConfig.onExpired,
47
+ persist: authConfig.persist,
48
+ },
49
+ context
50
+ );
51
+
52
+ default:
53
+ throw new Error(`Unknown auth type: ${(authConfig as { type: string }).type}`);
54
+ }
55
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * SSO Token Authentication Strategy
3
+ *
4
+ * Exchanges a SAML token for a QuickBase temporary token via OAuth token exchange.
5
+ */
6
+
7
+ import type { AuthStrategy, AuthContext } from './types.js';
8
+ import type { Logger } from '../core/logger.js';
9
+ import { TokenCache } from './token-cache.js';
10
+ import { DEFAULT_CONFIG } from '../core/types.js';
11
+ import { AuthenticationError } from '../core/errors.js';
12
+
13
+ export class SsoTokenStrategy implements AuthStrategy {
14
+ private readonly samlToken: string;
15
+ private readonly realm: string;
16
+ private readonly baseUrl: string;
17
+ private readonly fetchApi: typeof fetch;
18
+ private readonly logger: Logger;
19
+ private readonly cache: TokenCache;
20
+
21
+ /**
22
+ * Track pending SSO token exchange to deduplicate concurrent requests
23
+ */
24
+ private pendingExchange: Promise<string> | null = null;
25
+
26
+ constructor(
27
+ samlToken: string,
28
+ context: AuthContext,
29
+ options?: { tokenLifespanMs?: number }
30
+ ) {
31
+ this.samlToken = samlToken;
32
+ this.realm = context.realm;
33
+ this.baseUrl = context.baseUrl;
34
+ this.fetchApi = context.fetchApi;
35
+ this.logger = context.logger;
36
+ // SSO tokens are global, not per-dbid, so we use a single cache key
37
+ this.cache = new TokenCache(
38
+ options?.tokenLifespanMs ?? DEFAULT_CONFIG.tempTokenLifespanMs
39
+ );
40
+ }
41
+
42
+ private get cacheKey(): string {
43
+ return '__sso__';
44
+ }
45
+
46
+ async getToken(_dbid?: string): Promise<string> {
47
+ // Check cache first
48
+ const cachedToken = this.cache.get(this.cacheKey);
49
+ if (cachedToken) {
50
+ this.logger.token('cache-hit');
51
+ return cachedToken;
52
+ }
53
+
54
+ this.logger.token('cache-miss');
55
+
56
+ // Check if there's already an exchange in progress
57
+ if (this.pendingExchange) {
58
+ this.logger.debug('Waiting for existing SSO token exchange');
59
+ return this.pendingExchange;
60
+ }
61
+
62
+ // Exchange SAML token for QuickBase token
63
+ const exchangePromise = this.exchangeToken();
64
+ this.pendingExchange = exchangePromise;
65
+
66
+ try {
67
+ return await exchangePromise;
68
+ } finally {
69
+ this.pendingExchange = null;
70
+ }
71
+ }
72
+
73
+ private async exchangeToken(): Promise<string> {
74
+ this.logger.token('fetch');
75
+
76
+ const url = `${this.baseUrl}/auth/oauth/token`;
77
+
78
+ const body = new URLSearchParams({
79
+ grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
80
+ requested_token_type: 'urn:quickbase:params:oauth:token-type:temp_token',
81
+ subject_token: this.samlToken,
82
+ subject_token_type: 'urn:ietf:params:oauth:token-type:saml2',
83
+ });
84
+
85
+ const response = await this.fetchApi(url, {
86
+ method: 'POST',
87
+ headers: {
88
+ 'QB-Realm-Hostname': `${this.realm}.quickbase.com`,
89
+ 'Content-Type': 'application/x-www-form-urlencoded',
90
+ },
91
+ body: body.toString(),
92
+ });
93
+
94
+ if (!response.ok) {
95
+ const errorText = await response.text();
96
+ throw new AuthenticationError(
97
+ `SSO token exchange failed: ${response.status} ${errorText}`,
98
+ { rayId: response.headers.get('qb-api-ray') || undefined }
99
+ );
100
+ }
101
+
102
+ const data = await response.json() as {
103
+ access_token?: string;
104
+ issued_token_type?: string;
105
+ token_type?: string;
106
+ };
107
+
108
+ const token = data.access_token;
109
+ if (!token) {
110
+ throw new AuthenticationError('No access_token in SSO exchange response');
111
+ }
112
+
113
+ // Cache the token
114
+ this.cache.set(this.cacheKey, token);
115
+ this.logger.debug('Cached new SSO token');
116
+
117
+ return token;
118
+ }
119
+
120
+ getAuthorizationHeader(token: string): string {
121
+ return `QB-TEMP-TOKEN ${token}`;
122
+ }
123
+
124
+ async handleAuthError(_dbid?: string): Promise<boolean> {
125
+ // Invalidate the cached token
126
+ this.invalidate();
127
+
128
+ // Try to exchange for a new token
129
+ try {
130
+ this.logger.token('refresh');
131
+ await this.getToken();
132
+ return true;
133
+ } catch (error) {
134
+ this.logger.error('Failed to refresh SSO token', error);
135
+ return false;
136
+ }
137
+ }
138
+
139
+ invalidate(_dbid?: string): void {
140
+ this.logger.token('expire');
141
+ this.cache.clear();
142
+ }
143
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Temporary Token Authentication Strategy
3
+ *
4
+ * Fetches database-specific temporary tokens from the QuickBase API.
5
+ * Tokens are cached and automatically refreshed when expired.
6
+ */
7
+
8
+ import type { AuthStrategy, AuthContext } from './types.js';
9
+ import type { Logger } from '../core/logger.js';
10
+ import { TokenCache } from './token-cache.js';
11
+ import { DEFAULT_CONFIG } from '../core/types.js';
12
+ import { AuthenticationError } from '../core/errors.js';
13
+
14
+ export class TempTokenStrategy implements AuthStrategy {
15
+ private readonly userToken?: string;
16
+ private readonly appToken?: string;
17
+ private readonly realm: string;
18
+ private readonly baseUrl: string;
19
+ private readonly fetchApi: typeof fetch;
20
+ private readonly logger: Logger;
21
+ private readonly cache: TokenCache;
22
+
23
+ /**
24
+ * Track pending token fetches to deduplicate concurrent requests
25
+ * This prevents multiple simultaneous API calls for the same dbid
26
+ */
27
+ private readonly pendingFetches = new Map<string, Promise<string>>();
28
+
29
+ constructor(
30
+ userToken: string | undefined,
31
+ context: AuthContext,
32
+ options?: { appToken?: string; tokenLifespanMs?: number }
33
+ ) {
34
+ this.userToken = userToken;
35
+ this.appToken = options?.appToken;
36
+ this.realm = context.realm;
37
+ this.baseUrl = context.baseUrl;
38
+ this.fetchApi = context.fetchApi;
39
+ this.logger = context.logger;
40
+ this.cache = new TokenCache(
41
+ options?.tokenLifespanMs ?? DEFAULT_CONFIG.tempTokenLifespanMs
42
+ );
43
+ }
44
+
45
+ async getToken(dbid?: string): Promise<string> {
46
+ if (!dbid) {
47
+ throw new Error('Database ID (dbid) is required for temporary token authentication');
48
+ }
49
+
50
+ // Check cache first
51
+ const cachedToken = this.cache.get(dbid);
52
+ if (cachedToken) {
53
+ this.logger.token('cache-hit', dbid);
54
+ return cachedToken;
55
+ }
56
+
57
+ this.logger.token('cache-miss', dbid);
58
+
59
+ // Check if there's already a fetch in progress for this dbid
60
+ const pendingFetch = this.pendingFetches.get(dbid);
61
+ if (pendingFetch) {
62
+ this.logger.debug(`Waiting for existing token fetch for dbid: ${dbid}`);
63
+ return pendingFetch;
64
+ }
65
+
66
+ // Fetch new token
67
+ const fetchPromise = this.fetchToken(dbid);
68
+ this.pendingFetches.set(dbid, fetchPromise);
69
+
70
+ try {
71
+ const token = await fetchPromise;
72
+ return token;
73
+ } finally {
74
+ this.pendingFetches.delete(dbid);
75
+ }
76
+ }
77
+
78
+ private async fetchToken(dbid: string): Promise<string> {
79
+ this.logger.token('fetch', dbid);
80
+
81
+ const url = `${this.baseUrl}/auth/temporary/${dbid}`;
82
+ const headers: Record<string, string> = {
83
+ 'QB-Realm-Hostname': `${this.realm}.quickbase.com`,
84
+ 'Content-Type': 'application/json',
85
+ };
86
+
87
+ // Only include Authorization header if we have a userToken
88
+ // For Code Pages, browser session cookies handle auth instead
89
+ if (this.userToken) {
90
+ headers['Authorization'] = `QB-USER-TOKEN ${this.userToken}`;
91
+ }
92
+
93
+ if (this.appToken) {
94
+ headers['QB-App-Token'] = this.appToken;
95
+ }
96
+
97
+ const response = await this.fetchApi(url, {
98
+ method: 'GET',
99
+ headers,
100
+ credentials: 'include', // Include cookies for Code Page auth
101
+ } as RequestInit);
102
+
103
+ if (!response.ok) {
104
+ const errorText = await response.text();
105
+ throw new AuthenticationError(
106
+ `Failed to fetch temporary token for ${dbid}: ${response.status} ${errorText}`,
107
+ { rayId: response.headers.get('qb-api-ray') || undefined }
108
+ );
109
+ }
110
+
111
+ const data = await response.json() as { temporaryAuthorization?: string };
112
+ const token = data.temporaryAuthorization;
113
+
114
+ if (!token) {
115
+ throw new AuthenticationError(
116
+ `No temporary token in response for ${dbid}`
117
+ );
118
+ }
119
+
120
+ // Cache the token
121
+ this.cache.set(dbid, token);
122
+ this.logger.debug(`Cached new token for dbid: ${dbid}`);
123
+
124
+ return token;
125
+ }
126
+
127
+ getAuthorizationHeader(token: string): string {
128
+ return `QB-TEMP-TOKEN ${token}`;
129
+ }
130
+
131
+ async handleAuthError(dbid?: string): Promise<boolean> {
132
+ if (!dbid) {
133
+ this.logger.debug('Auth error without dbid - cannot refresh temp token');
134
+ return false;
135
+ }
136
+
137
+ // Invalidate the cached token
138
+ this.invalidate(dbid);
139
+
140
+ // Try to fetch a new token
141
+ try {
142
+ this.logger.token('refresh', dbid);
143
+ await this.getToken(dbid);
144
+ return true;
145
+ } catch (error) {
146
+ this.logger.error(`Failed to refresh temp token for ${dbid}`, error);
147
+ return false;
148
+ }
149
+ }
150
+
151
+ invalidate(dbid?: string): void {
152
+ if (dbid) {
153
+ this.logger.token('expire', dbid);
154
+ this.cache.delete(dbid);
155
+ } else {
156
+ // Invalidate all tokens
157
+ this.cache.clear();
158
+ }
159
+ }
160
+ }