@glideidentity/glide-be-sdk-node 2.0.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/LICENSE +4 -0
- package/README.md +90 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +65 -0
- package/dist/glide.d.ts +31 -0
- package/dist/glide.d.ts.map +1 -0
- package/dist/glide.js +61 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/internal/http.d.ts +20 -0
- package/dist/internal/http.d.ts.map +1 -0
- package/dist/internal/http.js +89 -0
- package/dist/internal/oauth.d.ts +53 -0
- package/dist/internal/oauth.d.ts.map +1 -0
- package/dist/internal/oauth.js +139 -0
- package/dist/logger.d.ts +28 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +118 -0
- package/dist/services/magical-auth.d.ts +32 -0
- package/dist/services/magical-auth.d.ts.map +1 -0
- package/dist/services/magical-auth.js +265 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +24 -0
- package/dist/validation.d.ts +15 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +103 -0
- package/package.json +28 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OAuthClient = exports.OAuthError = exports.OAuthManager = void 0;
|
|
4
|
+
const http_1 = require("./http");
|
|
5
|
+
/** Default buffer time (seconds) before token expiry to trigger refresh. */
|
|
6
|
+
const DEFAULT_TOKEN_REFRESH_BUFFER_SECONDS = 60;
|
|
7
|
+
/**
|
|
8
|
+
* OAuth2 Client Credentials authentication manager.
|
|
9
|
+
* Manages token fetching and caching with automatic refresh before expiry.
|
|
10
|
+
*/
|
|
11
|
+
class OAuthManager {
|
|
12
|
+
constructor(clientId, clientSecret, baseUrl, logger, refreshBuffer = DEFAULT_TOKEN_REFRESH_BUFFER_SECONDS) {
|
|
13
|
+
this.cachedToken = null;
|
|
14
|
+
/** Pending token fetch promise for request deduplication. */
|
|
15
|
+
this.pendingTokenPromise = null;
|
|
16
|
+
this.clientId = clientId;
|
|
17
|
+
this.clientSecret = clientSecret;
|
|
18
|
+
this.tokenUrl = `${baseUrl}/oauth2-cc/token`;
|
|
19
|
+
this.refreshBuffer = refreshBuffer;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Gets a valid access token, fetching a new one if necessary.
|
|
24
|
+
* Implements lazy refresh - tokens are refreshed before they expire.
|
|
25
|
+
* Uses pending promise deduplication to prevent multiple concurrent token fetches.
|
|
26
|
+
*/
|
|
27
|
+
async getAccessToken() {
|
|
28
|
+
if (this.isTokenValid()) {
|
|
29
|
+
this.logger.debug('Using cached access token');
|
|
30
|
+
return this.cachedToken.accessToken;
|
|
31
|
+
}
|
|
32
|
+
// If a token fetch is already in progress, await the same promise
|
|
33
|
+
if (this.pendingTokenPromise) {
|
|
34
|
+
this.logger.debug('Awaiting pending token fetch');
|
|
35
|
+
return this.pendingTokenPromise;
|
|
36
|
+
}
|
|
37
|
+
this.logger.debug('Fetching new access token', { tokenUrl: this.tokenUrl });
|
|
38
|
+
// Store the promise to deduplicate concurrent requests
|
|
39
|
+
this.pendingTokenPromise = this.fetchAndCacheToken();
|
|
40
|
+
try {
|
|
41
|
+
return await this.pendingTokenPromise;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
// Clear pending promise when fetch completes (success or failure)
|
|
45
|
+
this.pendingTokenPromise = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fetches and caches a new token. Separated for promise deduplication.
|
|
50
|
+
*/
|
|
51
|
+
async fetchAndCacheToken() {
|
|
52
|
+
const token = await this.fetchToken();
|
|
53
|
+
this.cacheToken(token);
|
|
54
|
+
return token.access_token;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Checks if the cached token is still valid (not expired or about to expire).
|
|
58
|
+
*/
|
|
59
|
+
isTokenValid() {
|
|
60
|
+
if (!this.cachedToken) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const bufferMs = this.refreshBuffer * 1000;
|
|
65
|
+
const isValid = this.cachedToken.expiresAt - bufferMs > now;
|
|
66
|
+
if (!isValid) {
|
|
67
|
+
this.logger.debug('Cached token expired or within refresh buffer');
|
|
68
|
+
}
|
|
69
|
+
return isValid;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Fetches a new access token from the OAuth2 token endpoint.
|
|
73
|
+
* Uses HTTP Basic authentication with base64-encoded credentials.
|
|
74
|
+
* Most OAuth2 servers expect raw client credentials to be base64-encoded directly
|
|
75
|
+
* in the Authorization header without additional URL-encoding.
|
|
76
|
+
*
|
|
77
|
+
*/
|
|
78
|
+
async fetchToken() {
|
|
79
|
+
const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');
|
|
80
|
+
try {
|
|
81
|
+
const response = await (0, http_1.request)(this.tokenUrl, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
85
|
+
'Authorization': `Basic ${credentials}`,
|
|
86
|
+
'Accept': 'application/json',
|
|
87
|
+
},
|
|
88
|
+
body: 'grant_type=client_credentials',
|
|
89
|
+
});
|
|
90
|
+
const tokenResponse = response.json();
|
|
91
|
+
this.logger.debug('Access token obtained successfully', {
|
|
92
|
+
tokenType: tokenResponse.token_type,
|
|
93
|
+
expiresIn: tokenResponse.expires_in,
|
|
94
|
+
});
|
|
95
|
+
return tokenResponse;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof http_1.FetchError) {
|
|
99
|
+
this.logger.error('Failed to obtain access token', {
|
|
100
|
+
status: error.response.statusCode,
|
|
101
|
+
message: error.message,
|
|
102
|
+
});
|
|
103
|
+
throw new OAuthError(error.response.statusCode || 500, error.response.statusMessage || 'Unknown error');
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Caches the token with its expiration time.
|
|
110
|
+
*/
|
|
111
|
+
cacheToken(token) {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
this.cachedToken = {
|
|
114
|
+
accessToken: token.access_token,
|
|
115
|
+
expiresAt: now + (token.expires_in * 1000),
|
|
116
|
+
};
|
|
117
|
+
this.logger.debug('Token cached', {
|
|
118
|
+
expiresAt: new Date(this.cachedToken.expiresAt).toISOString(),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Clears the cached token. Useful for forcing a token refresh.
|
|
123
|
+
*/
|
|
124
|
+
clearCache() {
|
|
125
|
+
this.cachedToken = null;
|
|
126
|
+
this.logger.debug('Token cache cleared');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
exports.OAuthManager = OAuthManager;
|
|
130
|
+
exports.OAuthClient = OAuthManager;
|
|
131
|
+
class OAuthError extends Error {
|
|
132
|
+
constructor(statusCode, statusMessage) {
|
|
133
|
+
super(`OAuth2 token request failed: ${statusCode} ${statusMessage}`);
|
|
134
|
+
this.name = 'OAuthError';
|
|
135
|
+
this.statusCode = statusCode;
|
|
136
|
+
this.statusMessage = statusMessage;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
exports.OAuthError = OAuthError;
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { LogFormat } from './types';
|
|
2
|
+
/** Log severity levels. */
|
|
3
|
+
export declare enum LogLevel {
|
|
4
|
+
DEBUG = 0,
|
|
5
|
+
INFO = 1,
|
|
6
|
+
WARN = 2,
|
|
7
|
+
ERROR = 3
|
|
8
|
+
}
|
|
9
|
+
/** Structured log field. */
|
|
10
|
+
export interface LogField {
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
/** Logger interface for custom implementations. */
|
|
14
|
+
export interface Logger {
|
|
15
|
+
debug(message: string, fields?: LogField): void;
|
|
16
|
+
info(message: string, fields?: LogField): void;
|
|
17
|
+
warn(message: string, fields?: LogField): void;
|
|
18
|
+
error(message: string, fields?: LogField): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Creates a logger instance.
|
|
22
|
+
* @param logLevel - Log level. Defaults to GLIDE_LOG_LEVEL env var or INFO.
|
|
23
|
+
* @param logFormat - Output format: 'text' or 'json'.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createLogger(logLevel?: LogLevel, logFormat?: LogFormat): Logger;
|
|
26
|
+
/** Generates a unique request ID. */
|
|
27
|
+
export declare function generateRequestId(): string;
|
|
28
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,2BAA2B;AAC3B,oBAAY,QAAQ;IAChB,KAAK,IAAI;IACT,IAAI,IAAI;IACR,IAAI,IAAI;IACR,KAAK,IAAI;CACZ;AAED,4BAA4B;AAC5B,MAAM,WAAW,QAAQ;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,mDAAmD;AACnD,MAAM,WAAW,MAAM;IACnB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC/C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC/C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;CACnD;AAiHD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,SAAS,GAAG,MAAM,CAG/E;AAED,qCAAqC;AACrC,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogLevel = void 0;
|
|
4
|
+
exports.createLogger = createLogger;
|
|
5
|
+
exports.generateRequestId = generateRequestId;
|
|
6
|
+
/** Log severity levels. */
|
|
7
|
+
var LogLevel;
|
|
8
|
+
(function (LogLevel) {
|
|
9
|
+
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
10
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
11
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
12
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
13
|
+
})(LogLevel || (exports.LogLevel = LogLevel = {}));
|
|
14
|
+
const SENSITIVE_KEYS = [
|
|
15
|
+
'apikey', 'api_key', 'api-key', 'token', 'password', 'secret',
|
|
16
|
+
'credential', 'authorization', 'bearer', 'key'
|
|
17
|
+
];
|
|
18
|
+
const SENSITIVE_PATTERN = new RegExp(`(${SENSITIVE_KEYS.join('|')})([=:\\s"']+)([^\\s"'&,}{\\]\\)]+)`, 'gi');
|
|
19
|
+
function sanitizeMessage(message) {
|
|
20
|
+
return message.replace(SENSITIVE_PATTERN, '$1$2***');
|
|
21
|
+
}
|
|
22
|
+
function sanitizeValue(key, value) {
|
|
23
|
+
if (typeof value !== 'string') {
|
|
24
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
25
|
+
return sanitizeObject(value);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
const lowerKey = key.toLowerCase();
|
|
30
|
+
for (const pattern of SENSITIVE_KEYS) {
|
|
31
|
+
if (lowerKey.includes(pattern)) {
|
|
32
|
+
return '***';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (value.startsWith('eyJ') && value.split('.').length === 3) {
|
|
36
|
+
return '***';
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function sanitizeObject(obj) {
|
|
41
|
+
const result = {};
|
|
42
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
43
|
+
result[key] = sanitizeValue(key, value);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
class DefaultLogger {
|
|
48
|
+
constructor(level = LogLevel.INFO, prefix = '[Glide]', format = 'text') {
|
|
49
|
+
this.level = level;
|
|
50
|
+
this.prefix = prefix;
|
|
51
|
+
this.format = format;
|
|
52
|
+
}
|
|
53
|
+
debug(message, fields) {
|
|
54
|
+
if (this.level <= LogLevel.DEBUG)
|
|
55
|
+
this.log('DEBUG', message, fields);
|
|
56
|
+
}
|
|
57
|
+
info(message, fields) {
|
|
58
|
+
if (this.level <= LogLevel.INFO)
|
|
59
|
+
this.log('INFO', message, fields);
|
|
60
|
+
}
|
|
61
|
+
warn(message, fields) {
|
|
62
|
+
if (this.level <= LogLevel.WARN)
|
|
63
|
+
this.log('WARN', message, fields);
|
|
64
|
+
}
|
|
65
|
+
error(message, fields) {
|
|
66
|
+
if (this.level <= LogLevel.ERROR)
|
|
67
|
+
this.log('ERROR', message, fields);
|
|
68
|
+
}
|
|
69
|
+
log(level, message, fields) {
|
|
70
|
+
const timestamp = new Date().toISOString();
|
|
71
|
+
const sanitizedMessage = sanitizeMessage(message);
|
|
72
|
+
const sanitizedFields = fields ? sanitizeObject(fields) : {};
|
|
73
|
+
if (this.format === 'json') {
|
|
74
|
+
const logEntry = { timestamp, level, message: sanitizedMessage, ...sanitizedFields };
|
|
75
|
+
console.log(JSON.stringify(logEntry));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const fieldsStr = Object.keys(sanitizedFields).length > 0
|
|
79
|
+
? ' ' + Object.entries(sanitizedFields).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(' ')
|
|
80
|
+
: '';
|
|
81
|
+
const output = `${this.prefix} ${timestamp} [${level}] ${sanitizedMessage}${fieldsStr}`;
|
|
82
|
+
if (level === 'ERROR')
|
|
83
|
+
console.error(output);
|
|
84
|
+
else if (level === 'WARN')
|
|
85
|
+
console.warn(output);
|
|
86
|
+
else
|
|
87
|
+
console.log(output);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function parseLogLevel(level) {
|
|
91
|
+
switch (level.toLowerCase()) {
|
|
92
|
+
case 'debug': return LogLevel.DEBUG;
|
|
93
|
+
case 'info': return LogLevel.INFO;
|
|
94
|
+
case 'warn':
|
|
95
|
+
case 'warning': return LogLevel.WARN;
|
|
96
|
+
case 'error': return LogLevel.ERROR;
|
|
97
|
+
default: return LogLevel.INFO;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function getLogLevelFromEnv() {
|
|
101
|
+
const envLogLevel = process.env.GLIDE_LOG_LEVEL;
|
|
102
|
+
if (envLogLevel)
|
|
103
|
+
return parseLogLevel(envLogLevel);
|
|
104
|
+
return LogLevel.INFO;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Creates a logger instance.
|
|
108
|
+
* @param logLevel - Log level. Defaults to GLIDE_LOG_LEVEL env var or INFO.
|
|
109
|
+
* @param logFormat - Output format: 'text' or 'json'.
|
|
110
|
+
*/
|
|
111
|
+
function createLogger(logLevel, logFormat) {
|
|
112
|
+
const level = logLevel ?? getLogLevelFromEnv();
|
|
113
|
+
return new DefaultLogger(level, '[Glide]', logFormat || 'text');
|
|
114
|
+
}
|
|
115
|
+
/** Generates a unique request ID. */
|
|
116
|
+
function generateRequestId() {
|
|
117
|
+
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
118
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { GlideSdkSettings } from '../types';
|
|
2
|
+
import { Logger } from '../logger';
|
|
3
|
+
import { OAuthManager } from '../internal/oauth';
|
|
4
|
+
import type { PrepareRequest, PrepareResponse, VerifyPhoneNumberRequest, VerifyPhoneNumberResponse, GetPhoneNumberRequest, GetPhoneNumberResponse, ReportInvocationRequest, ReportInvocationResponse } from '@glideidentity/glide-be-sdk-node-core';
|
|
5
|
+
/** Handles SIM-based phone authentication via the Glide API. */
|
|
6
|
+
export declare class MagicalAuth {
|
|
7
|
+
private settings;
|
|
8
|
+
private oauthClient;
|
|
9
|
+
private logger;
|
|
10
|
+
constructor(settings: GlideSdkSettings, oauthClient: OAuthManager, logger: Logger);
|
|
11
|
+
/** Initiates a phone authentication session. */
|
|
12
|
+
prepare(req: PrepareRequest): Promise<PrepareResponse>;
|
|
13
|
+
/** Verifies a phone number matches the device. */
|
|
14
|
+
verifyPhoneNumber(req: VerifyPhoneNumberRequest): Promise<VerifyPhoneNumberResponse>;
|
|
15
|
+
/** Retrieves the phone number. */
|
|
16
|
+
getPhoneNumber(req: GetPhoneNumberRequest): Promise<GetPhoneNumberResponse>;
|
|
17
|
+
/**
|
|
18
|
+
* Report that an authentication flow was started.
|
|
19
|
+
*
|
|
20
|
+
* Returns the server response. For fire-and-forget usage, callers can ignore the response:
|
|
21
|
+
* ```typescript
|
|
22
|
+
* glide.magicAuth.reportInvocation({ session_id }).catch(() => {}); // fire-and-forget
|
|
23
|
+
* const result = await glide.magicAuth.reportInvocation({ session_id }); // with response
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @param req - Request containing session_id from prepare response
|
|
27
|
+
* @returns Response with success status
|
|
28
|
+
*/
|
|
29
|
+
reportInvocation(req: ReportInvocationRequest): Promise<ReportInvocationResponse>;
|
|
30
|
+
private handleError;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=magical-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"magical-auth.d.ts","sourceRoot":"","sources":["../../src/services/magical-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnC,OAAO,EAAE,YAAY,EAAc,MAAM,mBAAmB,CAAC;AAG7D,OAAO,KAAK,EACR,cAAc,EACd,eAAe,EACf,wBAAwB,EACxB,yBAAyB,EACzB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,EAC3B,MAAM,uCAAuC,CAAC;AAa/C,gEAAgE;AAChE,qBAAa,WAAW;IACpB,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,MAAM,CAAS;gBAEX,QAAQ,EAAE,gBAAgB,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM;IAMjF,gDAAgD;IAC1C,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAkF5D,kDAAkD;IAC5C,iBAAiB,CAAC,GAAG,EAAE,wBAAwB,GAAG,OAAO,CAAC,yBAAyB,CAAC;IA8C1F,kCAAkC;IAC5B,cAAc,CAAC,GAAG,EAAE,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IA8CjF;;;;;;;;;;;OAWG;IACG,gBAAgB,CAAC,GAAG,EAAE,uBAAuB,GAAG,OAAO,CAAC,wBAAwB,CAAC;IAsCvF,OAAO,CAAC,WAAW;CAyCtB"}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MagicalAuth = void 0;
|
|
4
|
+
const http_1 = require("../internal/http");
|
|
5
|
+
const oauth_1 = require("../internal/oauth");
|
|
6
|
+
const errors_1 = require("../errors");
|
|
7
|
+
const validation_1 = require("../validation");
|
|
8
|
+
function generateNonce(length = 32) {
|
|
9
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
10
|
+
let result = '';
|
|
11
|
+
const randomValues = new Uint8Array(length);
|
|
12
|
+
crypto.getRandomValues(randomValues);
|
|
13
|
+
for (let i = 0; i < length; i++) {
|
|
14
|
+
result += chars[randomValues[i] % chars.length];
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
/** Handles SIM-based phone authentication via the Glide API. */
|
|
19
|
+
class MagicalAuth {
|
|
20
|
+
constructor(settings, oauthClient, logger) {
|
|
21
|
+
this.settings = settings;
|
|
22
|
+
this.oauthClient = oauthClient;
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
}
|
|
25
|
+
/** Initiates a phone authentication session. */
|
|
26
|
+
async prepare(req) {
|
|
27
|
+
const hasParentSession = req.options?.parent_session_id;
|
|
28
|
+
if (!hasParentSession) {
|
|
29
|
+
const useCaseValidation = (0, validation_1.validateUseCaseRequirements)(req.use_case, req.phone_number, req.plmn);
|
|
30
|
+
if (!useCaseValidation.valid) {
|
|
31
|
+
throw new errors_1.MagicalAuthError({
|
|
32
|
+
code: useCaseValidation.errorCode || errors_1.ErrorCode.INVALID_USE_CASE,
|
|
33
|
+
message: useCaseValidation.error,
|
|
34
|
+
status: 400,
|
|
35
|
+
details: useCaseValidation.details
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (req.phone_number) {
|
|
40
|
+
const phoneValidation = (0, validation_1.validatePhoneNumber)(req.phone_number);
|
|
41
|
+
if (!phoneValidation.valid) {
|
|
42
|
+
throw new errors_1.MagicalAuthError({
|
|
43
|
+
code: errors_1.ErrorCode.INVALID_PHONE_NUMBER,
|
|
44
|
+
message: phoneValidation.error,
|
|
45
|
+
status: 400
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (req.plmn) {
|
|
50
|
+
const plmnValidation = (0, validation_1.validatePlmn)(req.plmn);
|
|
51
|
+
if (!plmnValidation.valid) {
|
|
52
|
+
throw new errors_1.MagicalAuthError({
|
|
53
|
+
code: errors_1.ErrorCode.MISSING_REQUIRED_FIELD,
|
|
54
|
+
message: plmnValidation.error,
|
|
55
|
+
status: 400
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const nonce = req.nonce || generateNonce();
|
|
60
|
+
const body = { nonce };
|
|
61
|
+
if (req.use_case)
|
|
62
|
+
body.use_case = req.use_case;
|
|
63
|
+
if (req.phone_number)
|
|
64
|
+
body.phone_number = req.phone_number;
|
|
65
|
+
if (req.plmn)
|
|
66
|
+
body.plmn = req.plmn;
|
|
67
|
+
if (req.consent_data)
|
|
68
|
+
body.consent_data = req.consent_data;
|
|
69
|
+
if (req.client_info) {
|
|
70
|
+
body.client_info = {
|
|
71
|
+
user_agent: req.client_info.user_agent || 'unknown',
|
|
72
|
+
platform: req.client_info.platform || 'unknown'
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (req.options)
|
|
76
|
+
body.options = req.options;
|
|
77
|
+
const url = `${this.settings.baseUrl}/magic-auth/v2/auth/prepare`;
|
|
78
|
+
this.logger.debug('Prepare request', { url });
|
|
79
|
+
try {
|
|
80
|
+
const accessToken = await this.oauthClient.getAccessToken();
|
|
81
|
+
const response = await (0, http_1.request)(url, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
'Accept': 'application/json',
|
|
86
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify(body),
|
|
89
|
+
});
|
|
90
|
+
const result = response.json();
|
|
91
|
+
this.logger.debug('Prepare completed', { strategy: result.authentication_strategy });
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
this.handleError(error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Verifies a phone number matches the device. */
|
|
99
|
+
async verifyPhoneNumber(req) {
|
|
100
|
+
if (!req.session?.session_key) {
|
|
101
|
+
throw new errors_1.MagicalAuthError({
|
|
102
|
+
code: errors_1.ErrorCode.MISSING_REQUIRED_FIELD,
|
|
103
|
+
message: 'Session is required',
|
|
104
|
+
status: 400
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (!req.credential) {
|
|
108
|
+
throw new errors_1.MagicalAuthError({
|
|
109
|
+
code: errors_1.ErrorCode.MISSING_REQUIRED_FIELD,
|
|
110
|
+
message: 'Credential is required',
|
|
111
|
+
status: 400
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const body = {
|
|
115
|
+
session: req.session,
|
|
116
|
+
credential: req.credential,
|
|
117
|
+
};
|
|
118
|
+
const url = `${this.settings.baseUrl}/magic-auth/v2/auth/verify-phone-number`;
|
|
119
|
+
this.logger.debug('VerifyPhoneNumber request', { url });
|
|
120
|
+
try {
|
|
121
|
+
const accessToken = await this.oauthClient.getAccessToken();
|
|
122
|
+
const response = await (0, http_1.request)(url, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: {
|
|
125
|
+
'Content-Type': 'application/json',
|
|
126
|
+
'Accept': 'application/json',
|
|
127
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
128
|
+
},
|
|
129
|
+
body: JSON.stringify(body),
|
|
130
|
+
});
|
|
131
|
+
const result = response.json();
|
|
132
|
+
this.logger.debug('VerifyPhoneNumber completed', { verified: result.verified });
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
this.handleError(error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Retrieves the phone number. */
|
|
140
|
+
async getPhoneNumber(req) {
|
|
141
|
+
if (!req.session?.session_key) {
|
|
142
|
+
throw new errors_1.MagicalAuthError({
|
|
143
|
+
code: errors_1.ErrorCode.MISSING_REQUIRED_FIELD,
|
|
144
|
+
message: 'Session is required',
|
|
145
|
+
status: 400
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (!req.credential) {
|
|
149
|
+
throw new errors_1.MagicalAuthError({
|
|
150
|
+
code: errors_1.ErrorCode.MISSING_REQUIRED_FIELD,
|
|
151
|
+
message: 'Credential is required',
|
|
152
|
+
status: 400
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const body = {
|
|
156
|
+
session: req.session,
|
|
157
|
+
credential: req.credential,
|
|
158
|
+
};
|
|
159
|
+
const url = `${this.settings.baseUrl}/magic-auth/v2/auth/get-phone-number`;
|
|
160
|
+
this.logger.debug('GetPhoneNumber request', { url });
|
|
161
|
+
try {
|
|
162
|
+
const accessToken = await this.oauthClient.getAccessToken();
|
|
163
|
+
const response = await (0, http_1.request)(url, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
'Accept': 'application/json',
|
|
168
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
169
|
+
},
|
|
170
|
+
body: JSON.stringify(body),
|
|
171
|
+
});
|
|
172
|
+
const result = response.json();
|
|
173
|
+
this.logger.debug('GetPhoneNumber completed', { phone_number: result.phone_number });
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
this.handleError(error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Report that an authentication flow was started.
|
|
182
|
+
*
|
|
183
|
+
* Returns the server response. For fire-and-forget usage, callers can ignore the response:
|
|
184
|
+
* ```typescript
|
|
185
|
+
* glide.magicAuth.reportInvocation({ session_id }).catch(() => {}); // fire-and-forget
|
|
186
|
+
* const result = await glide.magicAuth.reportInvocation({ session_id }); // with response
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* @param req - Request containing session_id from prepare response
|
|
190
|
+
* @returns Response with success status
|
|
191
|
+
*/
|
|
192
|
+
async reportInvocation(req) {
|
|
193
|
+
if (!req.session_id) {
|
|
194
|
+
throw new errors_1.MagicalAuthError({
|
|
195
|
+
code: errors_1.ErrorCode.MISSING_REQUIRED_FIELD,
|
|
196
|
+
message: 'session_id is required',
|
|
197
|
+
status: 400
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const body = {
|
|
201
|
+
session_id: req.session_id,
|
|
202
|
+
};
|
|
203
|
+
const url = `${this.settings.baseUrl}/magic-auth/v2/auth/report-invocation`;
|
|
204
|
+
this.logger.debug('ReportInvocation request', { url, session_id: req.session_id });
|
|
205
|
+
try {
|
|
206
|
+
const accessToken = await this.oauthClient.getAccessToken();
|
|
207
|
+
const response = await (0, http_1.request)(url, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: {
|
|
210
|
+
'Content-Type': 'application/json',
|
|
211
|
+
'Accept': 'application/json',
|
|
212
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
213
|
+
},
|
|
214
|
+
body: JSON.stringify(body),
|
|
215
|
+
});
|
|
216
|
+
const result = await response.json();
|
|
217
|
+
this.logger.debug('ReportInvocation completed', { session_id: req.session_id, success: result.success });
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
this.handleError(error);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
handleError(error) {
|
|
225
|
+
if (error instanceof http_1.FetchError) {
|
|
226
|
+
let errorData;
|
|
227
|
+
try {
|
|
228
|
+
errorData = JSON.parse(error.data);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
throw new errors_1.MagicalAuthError({
|
|
232
|
+
code: errors_1.ErrorCode.INTERNAL_SERVER_ERROR,
|
|
233
|
+
message: error.message,
|
|
234
|
+
status: error.response.statusCode || 500
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
throw new errors_1.MagicalAuthError({
|
|
238
|
+
...errorData,
|
|
239
|
+
status: error.response.statusCode || 500
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
// Wrap OAuth and other errors in MagicalAuthError for consistent error handling
|
|
243
|
+
if (error instanceof oauth_1.OAuthError) {
|
|
244
|
+
throw new errors_1.MagicalAuthError({
|
|
245
|
+
code: errors_1.ErrorCode.AUTHENTICATION_FAILED,
|
|
246
|
+
message: error.message,
|
|
247
|
+
status: error.statusCode || 401
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (error instanceof Error) {
|
|
251
|
+
throw new errors_1.MagicalAuthError({
|
|
252
|
+
code: errors_1.ErrorCode.INTERNAL_SERVER_ERROR,
|
|
253
|
+
message: error.message,
|
|
254
|
+
status: 500
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// Unknown error type - wrap with generic message
|
|
258
|
+
throw new errors_1.MagicalAuthError({
|
|
259
|
+
code: errors_1.ErrorCode.INTERNAL_SERVER_ERROR,
|
|
260
|
+
message: 'An unexpected error occurred',
|
|
261
|
+
status: 500
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
exports.MagicalAuth = MagicalAuth;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Logger, LogLevel } from './logger';
|
|
2
|
+
/** Log output format. */
|
|
3
|
+
export type LogFormat = 'json' | 'text';
|
|
4
|
+
/** OAuth2 token response from the token endpoint. */
|
|
5
|
+
export interface TokenResponse {
|
|
6
|
+
/** The access token to use for API calls. */
|
|
7
|
+
access_token: string;
|
|
8
|
+
/** Token type (typically "Bearer"). */
|
|
9
|
+
token_type: string;
|
|
10
|
+
/** Grant type used. */
|
|
11
|
+
grant_type: string;
|
|
12
|
+
/** Token lifetime in seconds. */
|
|
13
|
+
expires_in: number;
|
|
14
|
+
}
|
|
15
|
+
/** Cached token with expiration tracking. */
|
|
16
|
+
export interface CachedToken {
|
|
17
|
+
/** The access token. */
|
|
18
|
+
accessToken: string;
|
|
19
|
+
/** Timestamp (ms) when the token expires. */
|
|
20
|
+
expiresAt: number;
|
|
21
|
+
}
|
|
22
|
+
/** Client configuration options. */
|
|
23
|
+
export interface GlideSdkSettings {
|
|
24
|
+
/** OAuth2 Client ID (required). */
|
|
25
|
+
clientId: string;
|
|
26
|
+
/** OAuth2 Client Secret (required). */
|
|
27
|
+
clientSecret: string;
|
|
28
|
+
/** API base URL. Defaults to production. */
|
|
29
|
+
baseUrl?: string;
|
|
30
|
+
/** Log output format. */
|
|
31
|
+
logFormat?: LogFormat;
|
|
32
|
+
/** Log level. */
|
|
33
|
+
logLevel?: LogLevel;
|
|
34
|
+
/** Custom logger implementation. */
|
|
35
|
+
logger?: Logger;
|
|
36
|
+
/** Buffer time in seconds before token expiry to trigger refresh. Defaults to 60. */
|
|
37
|
+
tokenRefreshBuffer?: number;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAE5C,yBAAyB;AACzB,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAExC,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC1B,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,uBAAuB;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,6CAA6C;AAC7C,MAAM,WAAW,WAAW;IACxB,wBAAwB;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,oCAAoC;AACpC,MAAM,WAAW,gBAAgB;IAC7B,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,iBAAiB;IACjB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qFAAqF;IACrF,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC/B"}
|
package/dist/types.js
ADDED
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PrepareResponse } from '@glideidentity/glide-be-sdk-node-core';
|
|
2
|
+
/**
|
|
3
|
+
* Extract status_url from PrepareResponse strategy-specific data.
|
|
4
|
+
* Works for both Link and Desktop strategies.
|
|
5
|
+
*
|
|
6
|
+
* @param response - The prepare response
|
|
7
|
+
* @returns Status URL for polling, or undefined if not available (e.g., TS43 strategy)
|
|
8
|
+
*/
|
|
9
|
+
export declare function getStatusUrl(response: PrepareResponse): string | undefined;
|
|
10
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAiD,MAAM,uCAAuC,CAAC;AAEvH;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAe1E"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getStatusUrl = getStatusUrl;
|
|
4
|
+
const glide_be_sdk_node_core_1 = require("@glideidentity/glide-be-sdk-node-core");
|
|
5
|
+
/**
|
|
6
|
+
* Extract status_url from PrepareResponse strategy-specific data.
|
|
7
|
+
* Works for both Link and Desktop strategies.
|
|
8
|
+
*
|
|
9
|
+
* @param response - The prepare response
|
|
10
|
+
* @returns Status URL for polling, or undefined if not available (e.g., TS43 strategy)
|
|
11
|
+
*/
|
|
12
|
+
function getStatusUrl(response) {
|
|
13
|
+
if (!response?.data || !response.authentication_strategy) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
if (response.authentication_strategy === glide_be_sdk_node_core_1.AuthenticationStrategy.LINK) {
|
|
17
|
+
return response.data.status_url;
|
|
18
|
+
}
|
|
19
|
+
if (response.authentication_strategy === glide_be_sdk_node_core_1.AuthenticationStrategy.DESKTOP) {
|
|
20
|
+
return response.data.data?.status_url;
|
|
21
|
+
}
|
|
22
|
+
// TS43 strategy doesn't use polling
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|