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.
- package/README.md +144 -76
- package/dist/quickbase.esm.js +3 -3
- package/dist/quickbase.min.js +3 -3
- package/dist/quickbase.min.js.map +1 -1
- package/package.json +13 -9
- package/src/auth/index.ts +55 -0
- package/src/auth/sso-token.ts +143 -0
- package/src/auth/temp-token.ts +160 -0
- package/src/auth/ticket.ts +329 -0
- package/src/auth/token-cache.ts +97 -0
- package/src/auth/types.ts +50 -0
- package/src/auth/user-token.ts +38 -0
- package/src/cli/schema.ts +512 -0
- package/src/client/index.ts +36 -0
- package/src/client/pagination.ts +337 -0
- package/src/client/request.ts +544 -0
- package/src/client/retry.ts +169 -0
- package/src/client/throttle.ts +105 -0
- package/src/core/config.ts +160 -0
- package/src/core/dates.ts +63 -0
- package/src/core/errors.ts +349 -0
- package/src/core/index.ts +50 -0
- package/src/core/logger.ts +113 -0
- package/src/core/schema.ts +213 -0
- package/src/core/transform.ts +295 -0
- package/src/core/types.ts +337 -0
- package/src/generated/client.ts +1785 -0
- package/src/generated/operations.json +672 -0
- package/src/generated/types.ts +4884 -0
- package/src/index.ts +259 -0
- package/src/xml/codepages.ts +119 -0
- package/src/xml/dbvars.ts +75 -0
- package/src/xml/errors.ts +133 -0
- package/src/xml/fields.ts +121 -0
- package/src/xml/groups.ts +560 -0
- package/src/xml/html.ts +152 -0
- package/src/xml/index.ts +1196 -0
- package/src/xml/metadata.ts +111 -0
- package/src/xml/records.ts +301 -0
- package/src/xml/request.ts +238 -0
- package/src/xml/roles.ts +321 -0
- package/src/xml/schema.ts +436 -0
- package/src/xml/transform.ts +381 -0
- package/src/xml/types.ts +602 -0
- package/src/xml/users.ts +221 -0
- package/src/xml/webhooks.ts +260 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ticket Authentication Strategy (XML API)
|
|
3
|
+
*
|
|
4
|
+
* Authenticates using username/password via the XML API_Authenticate endpoint.
|
|
5
|
+
* Unlike user tokens, tickets properly attribute record changes (createdBy/modifiedBy)
|
|
6
|
+
* to the authenticated user.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: This uses the legacy QuickBase XML API (API_Authenticate).
|
|
9
|
+
* If QuickBase discontinues the XML API in the future, this file and related code
|
|
10
|
+
* can be safely removed.
|
|
11
|
+
*
|
|
12
|
+
* To find all related code, search for the marker: XML-API-TICKET
|
|
13
|
+
*
|
|
14
|
+
* Files to remove/update:
|
|
15
|
+
* - src/auth/ticket.ts (this file)
|
|
16
|
+
* - src/auth/index.ts (TicketStrategy export, createAuthStrategy case)
|
|
17
|
+
* - src/core/types.ts (TicketAuthConfig)
|
|
18
|
+
* - src/index.ts (exports)
|
|
19
|
+
* - tests/auth/ticket.test.ts
|
|
20
|
+
* - README.md: "Ticket Auth" section
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { AuthStrategy, AuthContext } from './types.js';
|
|
24
|
+
import type { Logger } from '../core/logger.js';
|
|
25
|
+
|
|
26
|
+
interface AuthenticateResponse {
|
|
27
|
+
ticket: string;
|
|
28
|
+
userid: string;
|
|
29
|
+
errcode: number;
|
|
30
|
+
errtext: string;
|
|
31
|
+
errdetail?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Options for ticket authentication
|
|
36
|
+
*/
|
|
37
|
+
export interface TicketAuthOptions {
|
|
38
|
+
/** Ticket validity in hours (default: 12, max: 4380 ~6 months) */
|
|
39
|
+
hours?: number;
|
|
40
|
+
/** Callback invoked when ticket expires - use to show login UI */
|
|
41
|
+
onExpired?: () => void;
|
|
42
|
+
/** Persist ticket to storage for session survival (default: none) */
|
|
43
|
+
persist?: 'sessionStorage' | 'localStorage';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Data structure stored in persistent storage */
|
|
47
|
+
interface PersistedTicketData {
|
|
48
|
+
ticket: string;
|
|
49
|
+
userId: string;
|
|
50
|
+
expiresAt: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Ticket authentication strategy using XML API_Authenticate.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const client = createClient({
|
|
59
|
+
* realm: 'mycompany',
|
|
60
|
+
* auth: {
|
|
61
|
+
* type: 'ticket',
|
|
62
|
+
* username: 'user@example.com',
|
|
63
|
+
* password: 'password',
|
|
64
|
+
* },
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* XML-API-TICKET: Remove this class if XML API is discontinued.
|
|
69
|
+
*/
|
|
70
|
+
export class TicketStrategy implements AuthStrategy {
|
|
71
|
+
private readonly username: string;
|
|
72
|
+
private password: string | null; // Cleared after first auth
|
|
73
|
+
private readonly hours: number;
|
|
74
|
+
private readonly realm: string;
|
|
75
|
+
private readonly fetchApi: typeof fetch;
|
|
76
|
+
private readonly logger: Logger;
|
|
77
|
+
private readonly onExpired?: () => void;
|
|
78
|
+
private readonly persist?: 'sessionStorage' | 'localStorage';
|
|
79
|
+
private readonly storageKey: string;
|
|
80
|
+
|
|
81
|
+
private ticket: string | null = null;
|
|
82
|
+
private userId: string | null = null;
|
|
83
|
+
private expiresAt: number | null = null;
|
|
84
|
+
private authenticated = false;
|
|
85
|
+
private pendingAuth: Promise<string> | null = null;
|
|
86
|
+
|
|
87
|
+
constructor(
|
|
88
|
+
username: string,
|
|
89
|
+
password: string,
|
|
90
|
+
options: TicketAuthOptions | undefined,
|
|
91
|
+
context: AuthContext
|
|
92
|
+
) {
|
|
93
|
+
this.username = username;
|
|
94
|
+
this.password = password;
|
|
95
|
+
this.hours = Math.min(Math.max(options?.hours ?? 12, 1), 4380);
|
|
96
|
+
this.onExpired = options?.onExpired;
|
|
97
|
+
this.persist = options?.persist;
|
|
98
|
+
this.realm = context.realm;
|
|
99
|
+
this.fetchApi = context.fetchApi;
|
|
100
|
+
this.logger = context.logger;
|
|
101
|
+
this.storageKey = `qb_ticket_${this.realm}`;
|
|
102
|
+
|
|
103
|
+
// Try to restore ticket from storage
|
|
104
|
+
this.restoreFromStorage();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async getToken(_dbid?: string): Promise<string> {
|
|
108
|
+
// Return cached ticket if available and not expired
|
|
109
|
+
if (this.ticket && this.expiresAt && Date.now() < this.expiresAt) {
|
|
110
|
+
return this.ticket;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// If ticket existed but expired, clear it
|
|
114
|
+
if (this.ticket && this.expiresAt && Date.now() >= this.expiresAt) {
|
|
115
|
+
this.clearTicket();
|
|
116
|
+
this.onExpired?.();
|
|
117
|
+
throw new Error('Ticket expired; create a new client with fresh credentials');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// If already authenticated but no ticket, it expired
|
|
121
|
+
if (this.authenticated) {
|
|
122
|
+
this.onExpired?.();
|
|
123
|
+
throw new Error('Ticket expired; create a new client with fresh credentials');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Dedupe concurrent auth requests
|
|
127
|
+
if (this.pendingAuth) {
|
|
128
|
+
return this.pendingAuth;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.pendingAuth = this.authenticate();
|
|
132
|
+
try {
|
|
133
|
+
const ticket = await this.pendingAuth;
|
|
134
|
+
return ticket;
|
|
135
|
+
} finally {
|
|
136
|
+
this.pendingAuth = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getAuthorizationHeader(token: string): string {
|
|
141
|
+
return `QB-TICKET ${token}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async handleAuthError(_dbid?: string): Promise<boolean> {
|
|
145
|
+
// Clear expired ticket
|
|
146
|
+
this.clearTicket();
|
|
147
|
+
|
|
148
|
+
// Can't refresh - password was cleared after initial auth
|
|
149
|
+
this.logger.debug('Ticket auth error - cannot refresh (password was cleared)');
|
|
150
|
+
this.onExpired?.();
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
invalidate(_dbid?: string): void {
|
|
155
|
+
this.clearTicket();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private clearTicket(): void {
|
|
159
|
+
this.ticket = null;
|
|
160
|
+
this.expiresAt = null;
|
|
161
|
+
this.clearFromStorage();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get the authenticated user's ID (available after first API call)
|
|
166
|
+
*/
|
|
167
|
+
getUserId(): string | null {
|
|
168
|
+
return this.userId;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private async authenticate(): Promise<string> {
|
|
172
|
+
if (!this.password) {
|
|
173
|
+
this.onExpired?.();
|
|
174
|
+
throw new Error(
|
|
175
|
+
'No password to sign in with (the ticket expired, or sign-in failed); create a new client with fresh credentials'
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const url = `https://${this.realm}.quickbase.com/db/main`;
|
|
180
|
+
|
|
181
|
+
// Build XML request body
|
|
182
|
+
const xmlBody = `<qdbapi>
|
|
183
|
+
<username>${this.escapeXml(this.username)}</username>
|
|
184
|
+
<password>${this.escapeXml(this.password)}</password>
|
|
185
|
+
<hours>${this.hours}</hours>
|
|
186
|
+
</qdbapi>`;
|
|
187
|
+
|
|
188
|
+
// Drop the password now, whether sign-in succeeds or fails: it is only
|
|
189
|
+
// needed for this one request.
|
|
190
|
+
this.password = null;
|
|
191
|
+
|
|
192
|
+
this.logger.debug(`Authenticating via API_Authenticate for ${this.username}`);
|
|
193
|
+
|
|
194
|
+
const response = await this.fetchApi(url, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: {
|
|
197
|
+
'Content-Type': 'application/xml',
|
|
198
|
+
'QUICKBASE-ACTION': 'API_Authenticate',
|
|
199
|
+
},
|
|
200
|
+
body: xmlBody,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const xmlText = await response.text();
|
|
204
|
+
const result = this.parseXmlResponse(xmlText);
|
|
205
|
+
|
|
206
|
+
if (result.errcode !== 0) {
|
|
207
|
+
const errMsg = result.errdetail || result.errtext || 'Unknown error';
|
|
208
|
+
throw new Error(`Authentication failed: ${errMsg} (code: ${result.errcode})`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!result.ticket) {
|
|
212
|
+
throw new Error('No ticket returned from API_Authenticate');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Store ticket and user ID
|
|
216
|
+
this.ticket = result.ticket;
|
|
217
|
+
this.userId = result.userid;
|
|
218
|
+
this.expiresAt = Date.now() + this.hours * 60 * 60 * 1000;
|
|
219
|
+
this.authenticated = true;
|
|
220
|
+
|
|
221
|
+
// Save to storage if persistence is enabled
|
|
222
|
+
this.saveToStorage();
|
|
223
|
+
|
|
224
|
+
this.logger.debug(`Authenticated successfully, userId: ${this.userId}`);
|
|
225
|
+
|
|
226
|
+
return this.ticket;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private parseXmlResponse(xml: string): AuthenticateResponse {
|
|
230
|
+
// Simple XML parsing for the specific response format
|
|
231
|
+
const getTagContent = (tag: string): string => {
|
|
232
|
+
const match = xml.match(new RegExp(`<${tag}>([^<]*)</${tag}>`));
|
|
233
|
+
return match?.[1] ?? '';
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
ticket: getTagContent('ticket'),
|
|
238
|
+
userid: getTagContent('userid'),
|
|
239
|
+
errcode: parseInt(getTagContent('errcode'), 10) || 0,
|
|
240
|
+
errtext: getTagContent('errtext'),
|
|
241
|
+
errdetail: getTagContent('errdetail') || undefined,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private escapeXml(str: string): string {
|
|
246
|
+
return str
|
|
247
|
+
.replace(/&/g, '&')
|
|
248
|
+
.replace(/</g, '<')
|
|
249
|
+
.replace(/>/g, '>')
|
|
250
|
+
.replace(/"/g, '"')
|
|
251
|
+
.replace(/'/g, ''');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private getStorage(): { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem(key: string): void } | null {
|
|
255
|
+
if (!this.persist) return null;
|
|
256
|
+
try {
|
|
257
|
+
// Check if we're in a browser environment
|
|
258
|
+
if (typeof globalThis === 'undefined') return null;
|
|
259
|
+
type StorageLike = { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem(key: string): void };
|
|
260
|
+
const win = globalThis as { localStorage?: StorageLike; sessionStorage?: StorageLike };
|
|
261
|
+
const storage = this.persist === 'localStorage' ? win.localStorage : win.sessionStorage;
|
|
262
|
+
return storage ?? null;
|
|
263
|
+
} catch {
|
|
264
|
+
// Storage may be disabled (e.g., private browsing)
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private saveToStorage(): void {
|
|
270
|
+
const storage = this.getStorage();
|
|
271
|
+
if (!storage || !this.ticket || !this.userId || !this.expiresAt) return;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const data: PersistedTicketData = {
|
|
275
|
+
ticket: this.ticket,
|
|
276
|
+
userId: this.userId,
|
|
277
|
+
expiresAt: this.expiresAt,
|
|
278
|
+
};
|
|
279
|
+
storage.setItem(this.storageKey, JSON.stringify(data));
|
|
280
|
+
this.logger.debug(`Ticket saved to ${this.persist}`);
|
|
281
|
+
} catch {
|
|
282
|
+
// Storage quota exceeded or other error - ignore
|
|
283
|
+
this.logger.debug(`Failed to save ticket to ${this.persist}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private restoreFromStorage(): void {
|
|
288
|
+
const storage = this.getStorage();
|
|
289
|
+
if (!storage) return;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const json = storage.getItem(this.storageKey);
|
|
293
|
+
if (!json) return;
|
|
294
|
+
|
|
295
|
+
const data: PersistedTicketData = JSON.parse(json);
|
|
296
|
+
|
|
297
|
+
// Check if ticket is still valid (with 1 minute buffer)
|
|
298
|
+
if (Date.now() >= data.expiresAt - 60000) {
|
|
299
|
+
storage.removeItem(this.storageKey);
|
|
300
|
+
this.logger.debug('Stored ticket expired, removed from storage');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.ticket = data.ticket;
|
|
305
|
+
this.userId = data.userId;
|
|
306
|
+
this.expiresAt = data.expiresAt;
|
|
307
|
+
this.authenticated = true;
|
|
308
|
+
// Password stays null since we have a valid ticket
|
|
309
|
+
this.password = null;
|
|
310
|
+
|
|
311
|
+
this.logger.debug(`Ticket restored from ${this.persist}`);
|
|
312
|
+
} catch {
|
|
313
|
+
// Invalid JSON or other error - clear storage
|
|
314
|
+
storage.removeItem(this.storageKey);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private clearFromStorage(): void {
|
|
319
|
+
const storage = this.getStorage();
|
|
320
|
+
if (!storage) return;
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
storage.removeItem(this.storageKey);
|
|
324
|
+
this.logger.debug(`Ticket cleared from ${this.persist}`);
|
|
325
|
+
} catch {
|
|
326
|
+
// Ignore errors
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token cache with TTL support
|
|
3
|
+
*
|
|
4
|
+
* Manages cached temporary tokens with expiration handling.
|
|
5
|
+
* Tokens are stored per-dbid since QuickBase temp tokens are database-specific.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CachedToken } from './types.js';
|
|
9
|
+
|
|
10
|
+
export class TokenCache {
|
|
11
|
+
private readonly cache = new Map<string, CachedToken>();
|
|
12
|
+
private readonly defaultLifespanMs: number;
|
|
13
|
+
|
|
14
|
+
constructor(defaultLifespanMs: number) {
|
|
15
|
+
this.defaultLifespanMs = defaultLifespanMs;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get a cached token if it exists and is not expired
|
|
20
|
+
*/
|
|
21
|
+
get(dbid: string): string | null {
|
|
22
|
+
const entry = this.cache.get(dbid);
|
|
23
|
+
if (!entry) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check if token is expired (with 10 second buffer)
|
|
28
|
+
if (Date.now() >= entry.expiresAt - 10000) {
|
|
29
|
+
this.cache.delete(dbid);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return entry.token;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Store a token with optional custom lifespan
|
|
38
|
+
*/
|
|
39
|
+
set(dbid: string, token: string, lifespanMs?: number): void {
|
|
40
|
+
const expiresAt = Date.now() + (lifespanMs ?? this.defaultLifespanMs);
|
|
41
|
+
this.cache.set(dbid, { token, expiresAt });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Remove a specific token from cache
|
|
46
|
+
*/
|
|
47
|
+
delete(dbid: string): boolean {
|
|
48
|
+
return this.cache.delete(dbid);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Clear all cached tokens
|
|
53
|
+
*/
|
|
54
|
+
clear(): void {
|
|
55
|
+
this.cache.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if a token exists and is valid
|
|
60
|
+
*/
|
|
61
|
+
has(dbid: string): boolean {
|
|
62
|
+
return this.get(dbid) !== null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get time until token expires (in ms), or null if not cached
|
|
67
|
+
*/
|
|
68
|
+
getTimeToExpiry(dbid: string): number | null {
|
|
69
|
+
const entry = this.cache.get(dbid);
|
|
70
|
+
if (!entry) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const ttl = entry.expiresAt - Date.now();
|
|
74
|
+
return ttl > 0 ? ttl : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the number of cached tokens
|
|
79
|
+
*/
|
|
80
|
+
get size(): number {
|
|
81
|
+
// Clean up expired entries first
|
|
82
|
+
this.cleanup();
|
|
83
|
+
return this.cache.size;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Remove expired entries from cache
|
|
88
|
+
*/
|
|
89
|
+
private cleanup(): void {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
for (const [dbid, entry] of this.cache) {
|
|
92
|
+
if (now >= entry.expiresAt) {
|
|
93
|
+
this.cache.delete(dbid);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication strategy types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Logger } from '../core/logger.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Interface that all auth strategies must implement
|
|
9
|
+
*/
|
|
10
|
+
export interface AuthStrategy {
|
|
11
|
+
/**
|
|
12
|
+
* Get a valid token for the given database ID
|
|
13
|
+
* @param dbid - The database ID (required for temp tokens)
|
|
14
|
+
*/
|
|
15
|
+
getToken(dbid?: string): Promise<string>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the authorization header value
|
|
19
|
+
*/
|
|
20
|
+
getAuthorizationHeader(token: string): string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handle authentication errors (e.g., refresh token on 401)
|
|
24
|
+
* @returns true if token was refreshed and request should be retried
|
|
25
|
+
*/
|
|
26
|
+
handleAuthError(dbid?: string): Promise<boolean>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Invalidate cached tokens (if applicable)
|
|
30
|
+
*/
|
|
31
|
+
invalidate(dbid?: string): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Context passed to auth strategy constructors
|
|
36
|
+
*/
|
|
37
|
+
export interface AuthContext {
|
|
38
|
+
realm: string;
|
|
39
|
+
baseUrl: string;
|
|
40
|
+
fetchApi: typeof fetch;
|
|
41
|
+
logger: Logger;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Token cache entry
|
|
46
|
+
*/
|
|
47
|
+
export interface CachedToken {
|
|
48
|
+
token: string;
|
|
49
|
+
expiresAt: number;
|
|
50
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Token Authentication Strategy
|
|
3
|
+
*
|
|
4
|
+
* Simplest auth method - uses a persistent user token directly.
|
|
5
|
+
* Token is passed in the Authorization header as "QB-USER-TOKEN {token}"
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AuthStrategy, AuthContext } from './types.js';
|
|
9
|
+
import type { Logger } from '../core/logger.js';
|
|
10
|
+
|
|
11
|
+
export class UserTokenStrategy implements AuthStrategy {
|
|
12
|
+
private readonly userToken: string;
|
|
13
|
+
private readonly logger: Logger;
|
|
14
|
+
|
|
15
|
+
constructor(userToken: string, context: AuthContext) {
|
|
16
|
+
this.userToken = userToken;
|
|
17
|
+
this.logger = context.logger;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async getToken(_dbid?: string): Promise<string> {
|
|
21
|
+
// User tokens don't change, just return the configured token
|
|
22
|
+
return this.userToken;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getAuthorizationHeader(token: string): string {
|
|
26
|
+
return `QB-USER-TOKEN ${token}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async handleAuthError(_dbid?: string): Promise<boolean> {
|
|
30
|
+
// User tokens can't be refreshed - if auth fails, it's a permanent failure
|
|
31
|
+
this.logger.debug('Auth error with user token - cannot refresh');
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
invalidate(_dbid?: string): void {
|
|
36
|
+
// No-op for user tokens - they don't have cached state
|
|
37
|
+
}
|
|
38
|
+
}
|