@touchque/node 1.3.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/CHANGELOG.md +84 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/dist/index.d.mts +686 -0
- package/dist/index.d.ts +686 -0
- package/dist/index.js +804 -0
- package/dist/index.mjs +756 -0
- package/package.json +73 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
interface TouchQueConfig {
|
|
4
|
+
/** Your TouchQue API key (starts with `tq_`) */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Your TouchQue API secret */
|
|
7
|
+
apiSecret: string;
|
|
8
|
+
/**
|
|
9
|
+
* Optional base URL override (default:
|
|
10
|
+
* https://api-authenticator.touchque.com). Must be `https://` unless it
|
|
11
|
+
* points at localhost — the SDK refuses plaintext `http://` to any other
|
|
12
|
+
* host so the API key / signature are never sent in the clear.
|
|
13
|
+
*/
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
/** Request timeout in ms (default: 10000) */
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
interface GenerateSecretOptions {
|
|
19
|
+
/** The user's unique identifier in YOUR system (email, userId, etc.) */
|
|
20
|
+
externalUsername: string;
|
|
21
|
+
}
|
|
22
|
+
interface GenerateSecretResponse {
|
|
23
|
+
/** New setup secret — show to user once (e.g. as QR code) */
|
|
24
|
+
secret: string;
|
|
25
|
+
externalUsername: string;
|
|
26
|
+
/** ISO string of when this secret expires if not used */
|
|
27
|
+
expiresAt: string;
|
|
28
|
+
ttlMs: number;
|
|
29
|
+
/**
|
|
30
|
+
* 10 one-time recovery codes to be shown to the user.
|
|
31
|
+
* If the user loses their device, they can use one of these codes to bypass 2FA.
|
|
32
|
+
*/
|
|
33
|
+
recoveryCodes: string[];
|
|
34
|
+
}
|
|
35
|
+
interface UnlinkSecretOptions {
|
|
36
|
+
externalUsername: string;
|
|
37
|
+
}
|
|
38
|
+
interface UnlinkSecretResponse {
|
|
39
|
+
success: boolean;
|
|
40
|
+
externalUsername: string;
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
interface ValidateSecretOptions {
|
|
44
|
+
secret: string;
|
|
45
|
+
}
|
|
46
|
+
interface ValidateSecretResponse {
|
|
47
|
+
valid: boolean;
|
|
48
|
+
externalUsername?: string;
|
|
49
|
+
integrationId?: string;
|
|
50
|
+
integration?: {
|
|
51
|
+
companyName: string;
|
|
52
|
+
icon: string;
|
|
53
|
+
color: string;
|
|
54
|
+
};
|
|
55
|
+
message?: string;
|
|
56
|
+
reason?: string;
|
|
57
|
+
usedAt?: string;
|
|
58
|
+
expiredAt?: string;
|
|
59
|
+
}
|
|
60
|
+
interface ResetSecretOptions {
|
|
61
|
+
externalUsername: string;
|
|
62
|
+
}
|
|
63
|
+
interface ResetSecretResponse {
|
|
64
|
+
/** New setup secret — show to user once */
|
|
65
|
+
secret: string;
|
|
66
|
+
externalUsername: string;
|
|
67
|
+
expiresAt?: string;
|
|
68
|
+
recoveryCodes?: string[];
|
|
69
|
+
}
|
|
70
|
+
interface GetUserResponse {
|
|
71
|
+
externalUsername: string;
|
|
72
|
+
/** Set once the mobile app has linked (scanned) the secret. */
|
|
73
|
+
deviceId: string | null;
|
|
74
|
+
/** true once the secret has been linked to a device via /auth/secret/link. */
|
|
75
|
+
used: boolean;
|
|
76
|
+
frozen: boolean;
|
|
77
|
+
createdAt: string;
|
|
78
|
+
expireAt: string | null;
|
|
79
|
+
}
|
|
80
|
+
type LoginType = 'LOGIN' | 'DISABLE_2FA' | string;
|
|
81
|
+
interface LoginRequestOptions {
|
|
82
|
+
/** User identifier in YOUR system */
|
|
83
|
+
externalUsername: string;
|
|
84
|
+
/**
|
|
85
|
+
* The type of action requiring 2FA confirmation.
|
|
86
|
+
* Built-in types: 'LOGIN', 'DISABLE_2FA'
|
|
87
|
+
* Custom types can be created/configured in your TouchQue Dashboard.
|
|
88
|
+
*/
|
|
89
|
+
type: LoginType;
|
|
90
|
+
/** Optional reference (e.g. your internal transaction ID) */
|
|
91
|
+
referenceId?: string;
|
|
92
|
+
/** The IP address of the user initiating the request. TouchQue will resolve this to a City/Country. */
|
|
93
|
+
clientIp?: string;
|
|
94
|
+
/** The User-Agent string of the user's browser/device. */
|
|
95
|
+
userAgent?: string;
|
|
96
|
+
/** Require the user to authenticate with FaceID / TouchID on their device to approve this request. */
|
|
97
|
+
requireBiometric?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Force Number Matching (anti-push-bombing).
|
|
100
|
+
* When true, the user must type a matching code shown on their screen to approve.
|
|
101
|
+
* The backend also auto-enables this if MFA fatigue is detected (2+ failed requests in 10 min).
|
|
102
|
+
*/
|
|
103
|
+
requireNumberMatch?: boolean;
|
|
104
|
+
}
|
|
105
|
+
interface LoginRequestResponse {
|
|
106
|
+
requestId: string;
|
|
107
|
+
/** Challenge code for number matching UI (if enabled for this integration) */
|
|
108
|
+
challengeCode?: string;
|
|
109
|
+
message: string;
|
|
110
|
+
expiresAt: string;
|
|
111
|
+
/**
|
|
112
|
+
* Present only when this integration has behavioral biometrics enabled
|
|
113
|
+
* (TenantPolicy.behavioralBiometricsEnabled). Pass this down to your
|
|
114
|
+
* frontend along with `requestId` to initialize the behavioral widget in
|
|
115
|
+
* `@touchque/web` (`tq.behavioral.attach(...)`), scoped to your 2FA
|
|
116
|
+
* challenge UI. Single-purpose and short-lived — do not reuse across requests.
|
|
117
|
+
*/
|
|
118
|
+
telemetryToken?: string;
|
|
119
|
+
}
|
|
120
|
+
type LoginStatus = 'PENDING' | 'CONFIRMED' | 'REJECTED' | 'EXPIRED';
|
|
121
|
+
interface LoginStatusResponse {
|
|
122
|
+
status: LoginStatus;
|
|
123
|
+
}
|
|
124
|
+
interface WaitForApprovalOptions {
|
|
125
|
+
requestId: string;
|
|
126
|
+
/** How long to poll in total, ms (default: 30000 = 30s) */
|
|
127
|
+
timeout?: number;
|
|
128
|
+
/** Polling interval, ms (default: 1500) */
|
|
129
|
+
pollInterval?: number;
|
|
130
|
+
}
|
|
131
|
+
interface WaitForApprovalResult {
|
|
132
|
+
approved: boolean;
|
|
133
|
+
status: LoginStatus;
|
|
134
|
+
}
|
|
135
|
+
interface VerifyWebhookOptions {
|
|
136
|
+
/** Raw request body as string, exactly as received (before JSON.parse). */
|
|
137
|
+
rawBody: string;
|
|
138
|
+
/**
|
|
139
|
+
* The signature to check against. Pass the value of the `x-signature`
|
|
140
|
+
* header. Optional: if omitted, the SDK falls back to the `signature`
|
|
141
|
+
* field TouchQue also embeds in the JSON body.
|
|
142
|
+
*/
|
|
143
|
+
signature?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Reject the webhook if its `timestamp` is more than this many seconds
|
|
146
|
+
* from now (replay protection). Default 300 (5 minutes). Set to 0 to
|
|
147
|
+
* disable the freshness check.
|
|
148
|
+
*/
|
|
149
|
+
toleranceSeconds?: number;
|
|
150
|
+
}
|
|
151
|
+
interface WebhookPayload {
|
|
152
|
+
event: string;
|
|
153
|
+
requestId: string;
|
|
154
|
+
externalUsername: string;
|
|
155
|
+
status: LoginStatus;
|
|
156
|
+
timestamp: string;
|
|
157
|
+
[key: string]: unknown;
|
|
158
|
+
}
|
|
159
|
+
interface ProtectOptions<TArgs extends any[]> {
|
|
160
|
+
/** The action type (e.g. 'WITHDRAW', 'LOGIN') */
|
|
161
|
+
type: string;
|
|
162
|
+
/** Function to extract the externalUsername from the wrapped function's arguments */
|
|
163
|
+
getUserIdentifier: (...args: TArgs) => string;
|
|
164
|
+
/** Optional function to extract a referenceId from the arguments */
|
|
165
|
+
getReferenceId?: (...args: TArgs) => string | undefined;
|
|
166
|
+
/** Timeout in ms */
|
|
167
|
+
timeout?: number;
|
|
168
|
+
/** Polling interval in ms */
|
|
169
|
+
pollInterval?: number;
|
|
170
|
+
}
|
|
171
|
+
interface TouchQueErrorData {
|
|
172
|
+
error?: string;
|
|
173
|
+
message?: string;
|
|
174
|
+
code?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
declare class HttpClient {
|
|
178
|
+
private readonly client;
|
|
179
|
+
private readonly apiKey;
|
|
180
|
+
private readonly apiSecret;
|
|
181
|
+
constructor(config: TouchQueConfig);
|
|
182
|
+
/**
|
|
183
|
+
* Canonical signature.
|
|
184
|
+
* Format: HMAC(apiSecret, "METHOD:pathWithQuery:timestamp:nonce:bodyHash")
|
|
185
|
+
*
|
|
186
|
+
* `pathWithQuery` includes the sorted query string so GET/DELETE query
|
|
187
|
+
* params are covered by the signature (older SDKs signed the path only;
|
|
188
|
+
* the backend accepts either form).
|
|
189
|
+
*/
|
|
190
|
+
private sign;
|
|
191
|
+
private generateNonce;
|
|
192
|
+
private authHeaders;
|
|
193
|
+
post<T>(path: string, body?: Record<string, unknown>): Promise<T>;
|
|
194
|
+
get<T>(path: string, params?: Record<string, string | number | boolean>): Promise<T>;
|
|
195
|
+
delete<T>(path: string): Promise<T>;
|
|
196
|
+
private handleError;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
declare class Auth {
|
|
200
|
+
private readonly http;
|
|
201
|
+
constructor(http: HttpClient);
|
|
202
|
+
/**
|
|
203
|
+
* Generate a new setup secret for a user.
|
|
204
|
+
* Show this secret to the user ONCE (e.g. as a QR code) so they can
|
|
205
|
+
* link their TouchQue app.
|
|
206
|
+
*
|
|
207
|
+
* The secret expires after 60 seconds if not used (scanned by the mobile app).
|
|
208
|
+
* After expiry, calling this method again will automatically rotate the secret.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* const { secret, expiresAt } = await tq.auth.generateSecret({
|
|
212
|
+
* externalUsername: 'user@company.com'
|
|
213
|
+
* });
|
|
214
|
+
* // Show `secret` to user as QR code
|
|
215
|
+
* // Secret expires at `expiresAt` if not scanned
|
|
216
|
+
*/
|
|
217
|
+
generateSecret(options: GenerateSecretOptions): Promise<GenerateSecretResponse>;
|
|
218
|
+
/**
|
|
219
|
+
* Reset (regenerate) a user's secret.
|
|
220
|
+
* This invalidates the old secret and generates a new one.
|
|
221
|
+
* Show the new secret to the user.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* const { secret } = await tq.auth.resetSecret({
|
|
225
|
+
* externalUsername: 'user@company.com'
|
|
226
|
+
* });
|
|
227
|
+
* // Show new secret to user
|
|
228
|
+
*/
|
|
229
|
+
resetSecret(options: ResetSecretOptions): Promise<ResetSecretResponse>;
|
|
230
|
+
/**
|
|
231
|
+
* Validate a secret code.
|
|
232
|
+
* Useful if you want to verify the user entered the correct setup secret.
|
|
233
|
+
* Note: The backend allows this without an API key, but the SDK sends it for consistency.
|
|
234
|
+
*/
|
|
235
|
+
validateSecret(options: ValidateSecretOptions): Promise<ValidateSecretResponse>;
|
|
236
|
+
/**
|
|
237
|
+
* Look up a user's current link status — no push notification is sent.
|
|
238
|
+
* Useful for detecting "the mobile app just linked this secret" (`used`
|
|
239
|
+
* flips true, `deviceId` gets set) without sending a LOGIN challenge to
|
|
240
|
+
* their device, e.g. while polling during enrollment.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* const { used, deviceId } = await tq.auth.getUser({
|
|
244
|
+
* externalUsername: 'user@company.com'
|
|
245
|
+
* });
|
|
246
|
+
* if (used && deviceId) {
|
|
247
|
+
* // Device has linked — no approval push needed to confirm this.
|
|
248
|
+
* }
|
|
249
|
+
*/
|
|
250
|
+
getUser(options: {
|
|
251
|
+
externalUsername: string;
|
|
252
|
+
}): Promise<GetUserResponse>;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
declare class Login {
|
|
256
|
+
private readonly http;
|
|
257
|
+
constructor(http: HttpClient);
|
|
258
|
+
/**
|
|
259
|
+
* Request 2FA confirmation from a user's TouchQue app.
|
|
260
|
+
*
|
|
261
|
+
* The user will receive a push notification on their device.
|
|
262
|
+
* They can then approve or reject the request.
|
|
263
|
+
*
|
|
264
|
+
* Built-in `type` values:
|
|
265
|
+
* - `'LOGIN'` — User is logging in
|
|
266
|
+
* - `'DISABLE_2FA'` — User is disabling 2FA
|
|
267
|
+
* - Custom action type IDs or slugs configured in your TouchQue Dashboard
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* // Basic login verification
|
|
271
|
+
* const response = await tq.login.request({
|
|
272
|
+
* externalUsername: 'user@company.com',
|
|
273
|
+
* type: 'LOGIN'
|
|
274
|
+
* });
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* // Protect a custom action using its Dashboard-configured ID or slug
|
|
278
|
+
* const response = await tq.login.request({
|
|
279
|
+
* externalUsername: 'user@company.com',
|
|
280
|
+
* type: 'action_type_slug_or_id',
|
|
281
|
+
* referenceId: 'txn_abc123' // Your internal transaction ID
|
|
282
|
+
* });
|
|
283
|
+
*/
|
|
284
|
+
request(options: LoginRequestOptions): Promise<LoginRequestResponse>;
|
|
285
|
+
/**
|
|
286
|
+
* Check the current status of a login request.
|
|
287
|
+
*
|
|
288
|
+
* Returns: `'PENDING'` | `'CONFIRMED'` | `'REJECTED'` | `'EXPIRED'`
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* const { status } = await tq.login.status('request_id_here');
|
|
292
|
+
* if (status === 'CONFIRMED') {
|
|
293
|
+
* // User approved — proceed with action
|
|
294
|
+
* }
|
|
295
|
+
*/
|
|
296
|
+
status(requestId: string): Promise<LoginStatusResponse>;
|
|
297
|
+
/**
|
|
298
|
+
* Wait for the user to approve or reject a login request.
|
|
299
|
+
*
|
|
300
|
+
* This is a convenience method that polls the status endpoint
|
|
301
|
+
* until the request is approved, rejected, or times out.
|
|
302
|
+
*
|
|
303
|
+
* **This is the recommended way to integrate TouchQue into your routes.**
|
|
304
|
+
*
|
|
305
|
+
* @throws {TouchQueRejectedError} If the user rejects the request
|
|
306
|
+
* @throws {TouchQueTimeoutError} If the request times out (user didn't respond)
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* // Simple: Send request and wait for approval in one step
|
|
310
|
+
* try {
|
|
311
|
+
* const loginReq = await tq.login.request({
|
|
312
|
+
* externalUsername: 'user@company.com',
|
|
313
|
+
* type: 'LOGIN'
|
|
314
|
+
* });
|
|
315
|
+
*
|
|
316
|
+
* const result = await tq.login.waitForApproval({
|
|
317
|
+
* requestId: loginReq.requestId,
|
|
318
|
+
* timeout: 30000 // Wait up to 30 seconds
|
|
319
|
+
* });
|
|
320
|
+
*
|
|
321
|
+
* if (result.approved) {
|
|
322
|
+
* // ✅ User approved — proceed
|
|
323
|
+
* proceedWithAction();
|
|
324
|
+
* }
|
|
325
|
+
* } catch (err) {
|
|
326
|
+
* if (err instanceof TouchQueRejectedError) {
|
|
327
|
+
* // ❌ User rejected — cancel the action
|
|
328
|
+
* } else if (err instanceof TouchQueTimeoutError) {
|
|
329
|
+
* // ⏰ User didn't respond in time
|
|
330
|
+
* }
|
|
331
|
+
* }
|
|
332
|
+
*/
|
|
333
|
+
waitForApproval(options: WaitForApprovalOptions): Promise<WaitForApprovalResult>;
|
|
334
|
+
/**
|
|
335
|
+
* Convenience: Send a 2FA request AND wait for approval in one call.
|
|
336
|
+
*
|
|
337
|
+
* Perfect for protecting any action with a single function call.
|
|
338
|
+
*
|
|
339
|
+
* @throws {TouchQueRejectedError} User rejected
|
|
340
|
+
* @throws {TouchQueTimeoutError} User didn't respond in time
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* // Protect an action with 2FA — ONE LINE!
|
|
344
|
+
* const result = await tq.login.verify({
|
|
345
|
+
* externalUsername: 'user@company.com',
|
|
346
|
+
* type: 'action_type_slug_or_id',
|
|
347
|
+
* referenceId: 'action_789'
|
|
348
|
+
* });
|
|
349
|
+
* // If we reach here, user approved ✅
|
|
350
|
+
* executeAction();
|
|
351
|
+
*/
|
|
352
|
+
verify(options: LoginRequestOptions & {
|
|
353
|
+
timeout?: number;
|
|
354
|
+
pollInterval?: number;
|
|
355
|
+
}): Promise<WaitForApprovalResult & {
|
|
356
|
+
requestId: string;
|
|
357
|
+
challengeCode?: string;
|
|
358
|
+
}>;
|
|
359
|
+
/**
|
|
360
|
+
* Approve a pending 2FA request using a Recovery Code.
|
|
361
|
+
* Useful when a user loses their phone and needs to bypass 2FA
|
|
362
|
+
* using one of the backup codes generated during setup.
|
|
363
|
+
*/
|
|
364
|
+
approveWithRecoveryCode(options: {
|
|
365
|
+
requestId: string;
|
|
366
|
+
code: string;
|
|
367
|
+
}): Promise<{
|
|
368
|
+
success: boolean;
|
|
369
|
+
message: string;
|
|
370
|
+
}>;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
declare class Webhook {
|
|
374
|
+
private readonly apiSecret;
|
|
375
|
+
constructor(apiSecret: string);
|
|
376
|
+
/**
|
|
377
|
+
* Verify that an incoming webhook request is genuinely from TouchQue and
|
|
378
|
+
* return its parsed payload. Throws `TouchQueWebhookSignatureError` if the
|
|
379
|
+
* signature is invalid, the body is malformed, or the webhook is stale.
|
|
380
|
+
*
|
|
381
|
+
* **Always verify before processing, and de-duplicate on `payload.jti`
|
|
382
|
+
* (or `payload.requestId` + `payload.event`) so a replayed webhook can't
|
|
383
|
+
* re-trigger your logic.**
|
|
384
|
+
*
|
|
385
|
+
* @throws {TouchQueWebhookSignatureError}
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* // Express.js — mount with a raw body parser on this route
|
|
389
|
+
* app.post('/webhooks/touchque', express.raw({ type: 'application/json' }), (req, res) => {
|
|
390
|
+
* try {
|
|
391
|
+
* const event = tq.webhook.verify({
|
|
392
|
+
* rawBody: req.body.toString('utf8'),
|
|
393
|
+
* signature: req.headers['x-signature'] as string,
|
|
394
|
+
* });
|
|
395
|
+
* // ... handle event.event, guarded by an idempotency check on event.jti
|
|
396
|
+
* res.sendStatus(200);
|
|
397
|
+
* } catch {
|
|
398
|
+
* res.sendStatus(403); // not from TouchQue, tampered, or replayed
|
|
399
|
+
* }
|
|
400
|
+
* });
|
|
401
|
+
*/
|
|
402
|
+
verify(options: VerifyWebhookOptions): WebhookPayload;
|
|
403
|
+
/**
|
|
404
|
+
* Check a webhook signature without throwing. Returns `true` / `false`.
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* if (tq.webhook.isValid({ rawBody, signature })) {
|
|
408
|
+
* // process
|
|
409
|
+
* }
|
|
410
|
+
*/
|
|
411
|
+
isValid(options: VerifyWebhookOptions): boolean;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
interface WebAuthnRegisterOptionsRequest {
|
|
415
|
+
externalUsername: string;
|
|
416
|
+
/**
|
|
417
|
+
* Register a *resident* (discoverable) credential with forced user
|
|
418
|
+
* verification and a stable WebAuthn user handle — the shape a
|
|
419
|
+
* passwordless-primary login (see `primaryOptions`) authenticates against.
|
|
420
|
+
* Omit for the classic second-factor credential.
|
|
421
|
+
*/
|
|
422
|
+
discoverable?: boolean;
|
|
423
|
+
}
|
|
424
|
+
/** Passed straight through to navigator.credentials.create() in the browser. */
|
|
425
|
+
type WebAuthnRegistrationOptions = Record<string, unknown>;
|
|
426
|
+
interface WebAuthnRegisterVerifyRequest {
|
|
427
|
+
externalUsername: string;
|
|
428
|
+
/** The RegistrationResponseJSON returned by navigator.credentials.create(). */
|
|
429
|
+
response: Record<string, unknown>;
|
|
430
|
+
/** Optional display label for this credential (e.g. "MacBook Touch ID"). */
|
|
431
|
+
label?: string;
|
|
432
|
+
}
|
|
433
|
+
interface WebAuthnRegisterVerifyResponse {
|
|
434
|
+
verified: boolean;
|
|
435
|
+
credentialId: string;
|
|
436
|
+
}
|
|
437
|
+
interface WebAuthnAuthenticateOptionsRequest {
|
|
438
|
+
/** The pending LoginRequest id this WebAuthn assertion will approve. */
|
|
439
|
+
requestId: string;
|
|
440
|
+
}
|
|
441
|
+
/** Passed straight through to navigator.credentials.get() in the browser. */
|
|
442
|
+
type WebAuthnAuthenticationOptions = Record<string, unknown>;
|
|
443
|
+
interface WebAuthnAuthenticateVerifyRequest {
|
|
444
|
+
requestId: string;
|
|
445
|
+
/** The AuthenticationResponseJSON returned by navigator.credentials.get(). */
|
|
446
|
+
response: Record<string, unknown>;
|
|
447
|
+
}
|
|
448
|
+
interface WebAuthnAuthenticateVerifyResponse {
|
|
449
|
+
success: boolean;
|
|
450
|
+
message: string;
|
|
451
|
+
}
|
|
452
|
+
interface WebAuthnPrimaryOptionsRequest {
|
|
453
|
+
/** The account to authenticate (username-first passwordless-primary login). */
|
|
454
|
+
externalUsername: string;
|
|
455
|
+
}
|
|
456
|
+
interface WebAuthnPrimaryOptionsResponse {
|
|
457
|
+
/** Opaque id tying the browser assertion back to this attempt. */
|
|
458
|
+
attemptId: string;
|
|
459
|
+
/** Passed straight through to navigator.credentials.get() in the browser. */
|
|
460
|
+
options: Record<string, unknown>;
|
|
461
|
+
}
|
|
462
|
+
interface WebAuthnPrimaryVerifyRequest {
|
|
463
|
+
attemptId: string;
|
|
464
|
+
/** The AuthenticationResponseJSON returned by navigator.credentials.get(). */
|
|
465
|
+
response: Record<string, unknown>;
|
|
466
|
+
}
|
|
467
|
+
interface WebAuthnPrimaryVerifyResponse {
|
|
468
|
+
success: boolean;
|
|
469
|
+
/** Present when success:false — risk/policy demands a step-up (push/number-match). */
|
|
470
|
+
requiresStepUp?: boolean;
|
|
471
|
+
externalUsername: string;
|
|
472
|
+
riskScore: number;
|
|
473
|
+
/** Present when success:true — the CONFIRMED LoginRequest id. */
|
|
474
|
+
requestId?: string;
|
|
475
|
+
}
|
|
476
|
+
interface WebAuthnCredentialSummary {
|
|
477
|
+
id: string;
|
|
478
|
+
credentialId: string;
|
|
479
|
+
deviceType: string | null;
|
|
480
|
+
backedUp: boolean;
|
|
481
|
+
label: string | null;
|
|
482
|
+
createdAt: string;
|
|
483
|
+
lastUsedAt: string | null;
|
|
484
|
+
}
|
|
485
|
+
declare class WebAuthn {
|
|
486
|
+
private readonly http;
|
|
487
|
+
constructor(http: HttpClient);
|
|
488
|
+
/**
|
|
489
|
+
* Step 1 of registering a passkey: get options for `navigator.credentials.create()`.
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* const options = await tq.webauthn.registerOptions({ externalUsername: 'user@company.com' });
|
|
493
|
+
* // send `options` to the browser, call navigator.credentials.create({ publicKey: options })
|
|
494
|
+
*/
|
|
495
|
+
registerOptions(options: WebAuthnRegisterOptionsRequest): Promise<WebAuthnRegistrationOptions>;
|
|
496
|
+
/**
|
|
497
|
+
* Step 2: verify the browser's registration response and store the credential.
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* const { verified, credentialId } = await tq.webauthn.registerVerify({
|
|
501
|
+
* externalUsername: 'user@company.com',
|
|
502
|
+
* response: browserRegistrationResponseJSON,
|
|
503
|
+
* label: 'MacBook Touch ID',
|
|
504
|
+
* });
|
|
505
|
+
*/
|
|
506
|
+
registerVerify(options: WebAuthnRegisterVerifyRequest): Promise<WebAuthnRegisterVerifyResponse>;
|
|
507
|
+
/**
|
|
508
|
+
* Step 1 of approving a pending login with a passkey: get options for
|
|
509
|
+
* `navigator.credentials.get()`, scoped to a specific `requestId` (from
|
|
510
|
+
* `tq.login.request()`).
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* const loginReq = await tq.login.request({ externalUsername: 'user@company.com', type: 'LOGIN' });
|
|
514
|
+
* const options = await tq.webauthn.authenticateOptions({ requestId: loginReq.requestId });
|
|
515
|
+
*/
|
|
516
|
+
authenticateOptions(options: WebAuthnAuthenticateOptionsRequest): Promise<WebAuthnAuthenticationOptions>;
|
|
517
|
+
/**
|
|
518
|
+
* Step 2: verify the browser's assertion — approves the LoginRequest on success.
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* const result = await tq.webauthn.authenticateVerify({
|
|
522
|
+
* requestId: loginReq.requestId,
|
|
523
|
+
* response: browserAuthenticationResponseJSON,
|
|
524
|
+
* });
|
|
525
|
+
*/
|
|
526
|
+
authenticateVerify(options: WebAuthnAuthenticateVerifyRequest): Promise<WebAuthnAuthenticateVerifyResponse>;
|
|
527
|
+
/**
|
|
528
|
+
* Step 1 of a PASSWORDLESS-PRIMARY login: authenticate a user *from zero*
|
|
529
|
+
* (no password, no prior `tq.login.request()`, no mobile device) with a
|
|
530
|
+
* passkey registered via `registerOptions({ discoverable: true })`.
|
|
531
|
+
*
|
|
532
|
+
* Requires `TenantPolicy.passwordlessLoginEnabled` for the integration.
|
|
533
|
+
* Returns `404 no_passkey_registered` if the user has no passkey — the
|
|
534
|
+
* caller should then fall back to password login.
|
|
535
|
+
*
|
|
536
|
+
* @example
|
|
537
|
+
* const { attemptId, options } = await tq.webauthn.primaryOptions({ externalUsername: 'user@company.com' });
|
|
538
|
+
* // browser: const assertion = await navigator.credentials.get({ publicKey: options });
|
|
539
|
+
*/
|
|
540
|
+
primaryOptions(options: WebAuthnPrimaryOptionsRequest): Promise<WebAuthnPrimaryOptionsResponse>;
|
|
541
|
+
/**
|
|
542
|
+
* Step 2 of a passwordless-primary login: verify the browser assertion.
|
|
543
|
+
*
|
|
544
|
+
* On `success:true` the returned `requestId` is a CONFIRMED LoginRequest —
|
|
545
|
+
* the login is done. On `success:false` with `requiresStepUp:true`, risk
|
|
546
|
+
* or policy demands a second factor: start the normal `tq.login.request()`
|
|
547
|
+
* / number-match flow instead of trusting this assertion alone.
|
|
548
|
+
*/
|
|
549
|
+
primaryVerify(options: WebAuthnPrimaryVerifyRequest): Promise<WebAuthnPrimaryVerifyResponse>;
|
|
550
|
+
/**
|
|
551
|
+
* List a user's registered WebAuthn credentials (labels/metadata only, no key material).
|
|
552
|
+
*/
|
|
553
|
+
listCredentials(options: {
|
|
554
|
+
externalUsername: string;
|
|
555
|
+
}): Promise<{
|
|
556
|
+
credentials: WebAuthnCredentialSummary[];
|
|
557
|
+
}>;
|
|
558
|
+
/**
|
|
559
|
+
* Remove a registered credential (e.g. the user lost that device).
|
|
560
|
+
*/
|
|
561
|
+
deleteCredential(credentialRecordId: string): Promise<{
|
|
562
|
+
deleted: boolean;
|
|
563
|
+
}>;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Base error class for all TouchQue SDK errors.
|
|
568
|
+
*/
|
|
569
|
+
declare class TouchQueError extends Error {
|
|
570
|
+
constructor(message: string);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Thrown when the TouchQue API returns a non-2xx HTTP response.
|
|
574
|
+
* Contains the HTTP status code and the raw API error data.
|
|
575
|
+
*/
|
|
576
|
+
declare class TouchQueAPIError extends TouchQueError {
|
|
577
|
+
readonly status: number;
|
|
578
|
+
readonly code: string | undefined;
|
|
579
|
+
readonly data: TouchQueErrorData;
|
|
580
|
+
constructor(status: number, data: TouchQueErrorData);
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Thrown when the waitForApproval() polling method times out
|
|
584
|
+
* before the user approves or rejects the request.
|
|
585
|
+
*/
|
|
586
|
+
declare class TouchQueTimeoutError extends TouchQueError {
|
|
587
|
+
readonly requestId: string;
|
|
588
|
+
constructor(requestId: string, timeoutMs: number);
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Thrown when a login request is explicitly rejected by the user.
|
|
592
|
+
*/
|
|
593
|
+
declare class TouchQueRejectedError extends TouchQueError {
|
|
594
|
+
readonly requestId: string;
|
|
595
|
+
constructor(requestId: string);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Thrown when webhook signature verification fails.
|
|
599
|
+
* This means the incoming webhook is not from TouchQue (potential forgery).
|
|
600
|
+
*/
|
|
601
|
+
declare class TouchQueWebhookSignatureError extends TouchQueError {
|
|
602
|
+
constructor();
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Thrown when SDK is used before being properly configured.
|
|
606
|
+
*/
|
|
607
|
+
declare class TouchQueConfigError extends TouchQueError {
|
|
608
|
+
constructor(message: string);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
interface TouchQueMiddlewareOptions {
|
|
612
|
+
/**
|
|
613
|
+
* How long to wait for user approval in milliseconds.
|
|
614
|
+
* Default: 30000 (30 seconds)
|
|
615
|
+
*/
|
|
616
|
+
timeout?: number;
|
|
617
|
+
/**
|
|
618
|
+
* Function to extract the user's ID/Username from the Express request.
|
|
619
|
+
* By default, it looks for `req.user.email`, `req.user.id`, or `req.user.username`.
|
|
620
|
+
*/
|
|
621
|
+
getUserId?: (req: Request) => string | undefined;
|
|
622
|
+
/**
|
|
623
|
+
* Optional function to extract a reference ID (like a transaction ID) from the request.
|
|
624
|
+
*/
|
|
625
|
+
getReferenceId?: (req: Request) => string | undefined;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Creates an Express.js middleware that protects a route with TouchQue 2FA.
|
|
629
|
+
*
|
|
630
|
+
* @param tqClient - Your initialized TouchQue instance
|
|
631
|
+
* @param actionType - The type of action (e.g., 'LOGIN', or a custom action slug/ID)
|
|
632
|
+
* @param options - Custom timeout and ID extraction functions
|
|
633
|
+
*
|
|
634
|
+
* @example
|
|
635
|
+
* const tq = new TouchQue({ apiKey: '...', apiSecret: '...' });
|
|
636
|
+
*
|
|
637
|
+
* app.post('/api/sensitive-action',
|
|
638
|
+
* requireTouchQue(tq, 'action_type_slug_or_id'),
|
|
639
|
+
* (req, res) => { res.send('Success'); }
|
|
640
|
+
* );
|
|
641
|
+
*/
|
|
642
|
+
declare function requireTouchQue(tqClient: TouchQue, actionType: string, options?: TouchQueMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
643
|
+
|
|
644
|
+
declare class TouchQue {
|
|
645
|
+
private readonly client;
|
|
646
|
+
readonly auth: Auth;
|
|
647
|
+
readonly login: Login;
|
|
648
|
+
/** Webhook signature verification */
|
|
649
|
+
readonly webhook: Webhook;
|
|
650
|
+
/** WebAuthn/FIDO2 (passkey) registration and login approval */
|
|
651
|
+
readonly webauthn: WebAuthn;
|
|
652
|
+
/**
|
|
653
|
+
* Create a new TouchQue SDK client.
|
|
654
|
+
*
|
|
655
|
+
* @param config - Your API credentials from the TouchQue Dashboard
|
|
656
|
+
*
|
|
657
|
+
* @example
|
|
658
|
+
* import { TouchQue } from '@touchque/node';
|
|
659
|
+
*
|
|
660
|
+
* const tq = new TouchQue({
|
|
661
|
+
* apiKey: 'tq_auth_abc123',
|
|
662
|
+
* apiSecret: 'your_api_secret'
|
|
663
|
+
* });
|
|
664
|
+
*
|
|
665
|
+
* // Now use tq.auth, tq.login, tq.webhook
|
|
666
|
+
*/
|
|
667
|
+
constructor(config: TouchQueConfig);
|
|
668
|
+
/**
|
|
669
|
+
* Wrap and protect any async function with TouchQue 2FA.
|
|
670
|
+
*
|
|
671
|
+
* @param fn The function to protect
|
|
672
|
+
* @param options Configuration for extracting the user identity and 2FA type
|
|
673
|
+
* @returns A new function that requires 2FA approval before executing the original function
|
|
674
|
+
*
|
|
675
|
+
* @example
|
|
676
|
+
* const secureWithdraw = tq.protect(processWithdraw, {
|
|
677
|
+
* type: 'WITHDRAW',
|
|
678
|
+
* getUserIdentifier: (username, amount) => username
|
|
679
|
+
* });
|
|
680
|
+
*
|
|
681
|
+
* await secureWithdraw("user@company.com", 500); // Prompts 2FA automatically
|
|
682
|
+
*/
|
|
683
|
+
protect<TArgs extends any[], TReturn>(fn: (...args: TArgs) => Promise<TReturn> | TReturn, options: ProtectOptions<TArgs>): (...args: TArgs) => Promise<TReturn>;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export { Auth, type GenerateSecretOptions, type GenerateSecretResponse, type GetUserResponse, Login, type LoginRequestOptions, type LoginRequestResponse, type LoginStatus, type LoginStatusResponse, type LoginType, type ProtectOptions, type ResetSecretOptions, type ResetSecretResponse, TouchQue, TouchQueAPIError, type TouchQueConfig, TouchQueConfigError, TouchQueError, type TouchQueErrorData, type TouchQueMiddlewareOptions, TouchQueRejectedError, TouchQueTimeoutError, TouchQueWebhookSignatureError, type UnlinkSecretOptions, type UnlinkSecretResponse, type ValidateSecretOptions, type ValidateSecretResponse, type VerifyWebhookOptions, type WaitForApprovalOptions, type WaitForApprovalResult, WebAuthn, type WebAuthnAuthenticateOptionsRequest, type WebAuthnAuthenticateVerifyRequest, type WebAuthnAuthenticateVerifyResponse, type WebAuthnAuthenticationOptions, type WebAuthnCredentialSummary, type WebAuthnPrimaryOptionsRequest, type WebAuthnPrimaryOptionsResponse, type WebAuthnPrimaryVerifyRequest, type WebAuthnPrimaryVerifyResponse, type WebAuthnRegisterOptionsRequest, type WebAuthnRegisterVerifyRequest, type WebAuthnRegisterVerifyResponse, type WebAuthnRegistrationOptions, Webhook, type WebhookPayload, requireTouchQue };
|