@glideidentity/glide-be-sdk-node 2.0.1 → 2.1.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.
@@ -0,0 +1,110 @@
1
+ /** Cookie name prefix for device binding codes. Append the session key for the full name. */
2
+ export declare const BINDING_COOKIE_PREFIX = "_glide_bind_";
3
+ /** Cookie max-age in seconds (10 minutes, matches session TTL). */
4
+ export declare const BINDING_COOKIE_MAX_AGE = 600;
5
+ /**
6
+ * Returns the full cookie name for a given session key.
7
+ *
8
+ * Using a session-scoped name (`_glide_bind_{sessionKey}`) ensures that
9
+ * parallel `prepare()` calls from the same browser don't overwrite each
10
+ * other's cookies — each session gets its own cookie.
11
+ */
12
+ export declare function getBindingCookieName(sessionKey: string): string;
13
+ /**
14
+ * Regex matching a valid 64-character hex string (case-insensitive).
15
+ *
16
+ * Accepts both lowercase and uppercase hex so that external inputs (URL
17
+ * fragments, cookie values) still pass validation even if a proxy or
18
+ * browser extension uppercases them. Internal code normalises to
19
+ * lowercase before hashing or sending to the aggregator.
20
+ */
21
+ export declare const HEX_64: RegExp;
22
+ /**
23
+ * Asserts that the given value is a valid 64-character hex string.
24
+ * @throws {Error} If the value does not match.
25
+ */
26
+ export declare function assertValidFeCode(feCode: string): void;
27
+ /**
28
+ * Generates a cryptographically random device binding code.
29
+ * @returns 64-character lowercase hex string (32 random bytes).
30
+ */
31
+ export declare function generateFeCode(): string;
32
+ /**
33
+ * Computes the SHA-256 hash of a device binding code.
34
+ * @param feCode - The 64-character hex binding code (case-insensitive input, normalised to lowercase).
35
+ * @returns 64-character lowercase hex SHA-256 hash.
36
+ */
37
+ export declare function computeFeHash(feCode: string): string;
38
+ export interface BindingCookieOptions {
39
+ /** Cookie domain. Must match the completion redirect URL domain. */
40
+ domain?: string;
41
+ /** Cookie path. Defaults to '/'. */
42
+ path?: string;
43
+ /**
44
+ * Whether to set the Secure flag. Defaults to `true`.
45
+ *
46
+ * Set to `false` only for local development over plain HTTP.
47
+ * In production this MUST remain `true`.
48
+ */
49
+ secure?: boolean;
50
+ }
51
+ /**
52
+ * Builds the Set-Cookie header value to store the device binding code.
53
+ *
54
+ * @param feCode - The binding code to store (64-char hex, normalised to lowercase).
55
+ * @param sessionKey - The session key — used in the cookie name to avoid collisions between parallel sessions.
56
+ * @param options - Optional domain/path overrides.
57
+ * @returns A complete Set-Cookie header value string.
58
+ */
59
+ export declare function buildSetBindingCookieHeader(feCode: string, sessionKey: string, options?: BindingCookieOptions): string;
60
+ /**
61
+ * Builds the Set-Cookie header value to clear the device binding cookie.
62
+ *
63
+ * @param sessionKey - The session key — identifies which session's cookie to clear.
64
+ * @param options - Optional domain/path (must match the original cookie).
65
+ * @returns A complete Set-Cookie header value string that expires the cookie.
66
+ */
67
+ export declare function buildClearBindingCookieHeader(sessionKey: string, options?: BindingCookieOptions): string;
68
+ /**
69
+ * Parses the device binding code from a raw Cookie header string for a specific session.
70
+ *
71
+ * Returns `undefined` if the cookie is not present or the value is not
72
+ * a valid 64-character hex string. When found, the value is normalised
73
+ * to lowercase for deterministic hashing downstream.
74
+ *
75
+ * @param cookieHeader - The raw `Cookie` header value (e.g., "foo=bar; _glide_bind_abc123=...").
76
+ * @param sessionKey - The session key that identifies the cookie to look for.
77
+ * @returns The fe_code value (lowercase), or undefined if the cookie is not present or invalid.
78
+ */
79
+ export declare function parseBindingCookie(cookieHeader: string | undefined, sessionKey: string): string | undefined;
80
+ /**
81
+ * Builds Set-Cookie headers that expire all previous device binding cookies
82
+ * found in the request.
83
+ *
84
+ * Only one Link flow can be active per browser at a time (App Clip is a
85
+ * single native process), so stale cookies from abandoned flows must be
86
+ * cleared to prevent the old redirect from completing with an outdated
87
+ * binding code.
88
+ *
89
+ * @param cookieHeader - The raw `Cookie` request header.
90
+ * @param options - Cookie options (secure, domain, path) — must match the original set call.
91
+ * @returns Array of Set-Cookie header values that expire stale binding cookies.
92
+ */
93
+ export declare function clearStaleBindingCookies(cookieHeader: string | undefined, options?: BindingCookieOptions): string[];
94
+ /**
95
+ * Returns the HTML for the completion redirect page.
96
+ *
97
+ * This page extracts `agg_code` and `session_key` from the URL fragment,
98
+ * POSTs them to the developer's backend (which auto-includes the
99
+ * `_glide_bind_{sessionKey}` HttpOnly cookie), and displays the result.
100
+ *
101
+ * On successful completion, a **localStorage signal** is written
102
+ * (`glide_signal_{session_key}`) so the FE SDK in the original tab can
103
+ * detect completion via `StorageEvent` without polling. The signal is
104
+ * automatically cleaned up after 5 seconds.
105
+ *
106
+ * @param completeEndpoint - The developer backend endpoint to POST to (e.g., "/api/glide/complete").
107
+ * @returns Complete HTML string ready to serve.
108
+ */
109
+ export declare function getCompletionPageHtml(completeEndpoint: string): string;
110
+ //# sourceMappingURL=device-binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-binding.d.ts","sourceRoot":"","sources":["../src/device-binding.ts"],"names":[],"mappings":"AAMA,6FAA6F;AAC7F,eAAO,MAAM,qBAAqB,iBAAiB,CAAC;AAEpD,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAK/D;AAMD;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM,QAAsB,CAAC;AAE1C;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAItD;AAMD;;;GAGG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAIpD;AAMD,MAAM,WAAW,oBAAoB;IACjC,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAeD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAoBtH;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAoBxG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAU3G;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,MAAM,EAAE,CAmBnH;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAqGtE"}
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HEX_64 = exports.BINDING_COOKIE_MAX_AGE = exports.BINDING_COOKIE_PREFIX = void 0;
4
+ exports.getBindingCookieName = getBindingCookieName;
5
+ exports.assertValidFeCode = assertValidFeCode;
6
+ exports.generateFeCode = generateFeCode;
7
+ exports.computeFeHash = computeFeHash;
8
+ exports.buildSetBindingCookieHeader = buildSetBindingCookieHeader;
9
+ exports.buildClearBindingCookieHeader = buildClearBindingCookieHeader;
10
+ exports.parseBindingCookie = parseBindingCookie;
11
+ exports.clearStaleBindingCookies = clearStaleBindingCookies;
12
+ exports.getCompletionPageHtml = getCompletionPageHtml;
13
+ const crypto_1 = require("crypto");
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+ /** Cookie name prefix for device binding codes. Append the session key for the full name. */
18
+ exports.BINDING_COOKIE_PREFIX = '_glide_bind_';
19
+ /** Cookie max-age in seconds (10 minutes, matches session TTL). */
20
+ exports.BINDING_COOKIE_MAX_AGE = 600;
21
+ /**
22
+ * Returns the full cookie name for a given session key.
23
+ *
24
+ * Using a session-scoped name (`_glide_bind_{sessionKey}`) ensures that
25
+ * parallel `prepare()` calls from the same browser don't overwrite each
26
+ * other's cookies — each session gets its own cookie.
27
+ */
28
+ function getBindingCookieName(sessionKey) {
29
+ if (!sessionKey || UNSAFE_COOKIE_ATTR.test(sessionKey) || sessionKey.includes('=')) {
30
+ throw new Error('Invalid sessionKey for cookie name: must not be empty or contain ";", "=", "\\r", or "\\n"');
31
+ }
32
+ return `${exports.BINDING_COOKIE_PREFIX}${sessionKey}`;
33
+ }
34
+ // ---------------------------------------------------------------------------
35
+ // Shared validation
36
+ // ---------------------------------------------------------------------------
37
+ /**
38
+ * Regex matching a valid 64-character hex string (case-insensitive).
39
+ *
40
+ * Accepts both lowercase and uppercase hex so that external inputs (URL
41
+ * fragments, cookie values) still pass validation even if a proxy or
42
+ * browser extension uppercases them. Internal code normalises to
43
+ * lowercase before hashing or sending to the aggregator.
44
+ */
45
+ exports.HEX_64 = /^[0-9a-fA-F]{64}$/;
46
+ /**
47
+ * Asserts that the given value is a valid 64-character hex string.
48
+ * @throws {Error} If the value does not match.
49
+ */
50
+ function assertValidFeCode(feCode) {
51
+ if (!exports.HEX_64.test(feCode)) {
52
+ throw new Error('feCode must be a 64-character hex string');
53
+ }
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // Crypto utilities
57
+ // ---------------------------------------------------------------------------
58
+ /**
59
+ * Generates a cryptographically random device binding code.
60
+ * @returns 64-character lowercase hex string (32 random bytes).
61
+ */
62
+ function generateFeCode() {
63
+ return (0, crypto_1.randomBytes)(32).toString('hex');
64
+ }
65
+ /**
66
+ * Computes the SHA-256 hash of a device binding code.
67
+ * @param feCode - The 64-character hex binding code (case-insensitive input, normalised to lowercase).
68
+ * @returns 64-character lowercase hex SHA-256 hash.
69
+ */
70
+ function computeFeHash(feCode) {
71
+ assertValidFeCode(feCode);
72
+ // Normalise to lowercase so the hash is deterministic regardless of input case.
73
+ return (0, crypto_1.createHash)('sha256').update(feCode.toLowerCase()).digest('hex');
74
+ }
75
+ /** Characters that must not appear in cookie attribute values (prevents header/cookie injection). */
76
+ const UNSAFE_COOKIE_ATTR = /[;\r\n]/;
77
+ function validateCookieAttr(name, value) {
78
+ if (value === undefined)
79
+ return;
80
+ if (UNSAFE_COOKIE_ATTR.test(value)) {
81
+ throw new Error(`Invalid cookie ${name}: must not contain ';', '\\r', or '\\n'`);
82
+ }
83
+ if (name === 'path' && (value.length === 0 || !value.startsWith('/'))) {
84
+ throw new Error(`Invalid cookie path: must start with '/'`);
85
+ }
86
+ }
87
+ /**
88
+ * Builds the Set-Cookie header value to store the device binding code.
89
+ *
90
+ * @param feCode - The binding code to store (64-char hex, normalised to lowercase).
91
+ * @param sessionKey - The session key — used in the cookie name to avoid collisions between parallel sessions.
92
+ * @param options - Optional domain/path overrides.
93
+ * @returns A complete Set-Cookie header value string.
94
+ */
95
+ function buildSetBindingCookieHeader(feCode, sessionKey, options) {
96
+ assertValidFeCode(feCode);
97
+ validateCookieAttr('domain', options?.domain);
98
+ validateCookieAttr('path', options?.path);
99
+ const secure = options?.secure ?? true;
100
+ const cookieName = getBindingCookieName(sessionKey);
101
+ const parts = [
102
+ `${cookieName}=${feCode.toLowerCase()}`,
103
+ 'HttpOnly',
104
+ 'SameSite=Lax',
105
+ `Path=${options?.path ?? '/'}`,
106
+ `Max-Age=${exports.BINDING_COOKIE_MAX_AGE}`,
107
+ ];
108
+ if (secure) {
109
+ parts.push('Secure');
110
+ }
111
+ if (options?.domain) {
112
+ parts.push(`Domain=${options.domain}`);
113
+ }
114
+ return parts.join('; ');
115
+ }
116
+ /**
117
+ * Builds the Set-Cookie header value to clear the device binding cookie.
118
+ *
119
+ * @param sessionKey - The session key — identifies which session's cookie to clear.
120
+ * @param options - Optional domain/path (must match the original cookie).
121
+ * @returns A complete Set-Cookie header value string that expires the cookie.
122
+ */
123
+ function buildClearBindingCookieHeader(sessionKey, options) {
124
+ validateCookieAttr('domain', options?.domain);
125
+ validateCookieAttr('path', options?.path);
126
+ const secure = options?.secure ?? true;
127
+ const cookieName = getBindingCookieName(sessionKey);
128
+ const parts = [
129
+ `${cookieName}=`,
130
+ 'HttpOnly',
131
+ 'SameSite=Lax',
132
+ `Path=${options?.path ?? '/'}`,
133
+ 'Max-Age=0',
134
+ 'Expires=Thu, 01 Jan 1970 00:00:00 GMT',
135
+ ];
136
+ if (secure) {
137
+ parts.push('Secure');
138
+ }
139
+ if (options?.domain) {
140
+ parts.push(`Domain=${options.domain}`);
141
+ }
142
+ return parts.join('; ');
143
+ }
144
+ /**
145
+ * Parses the device binding code from a raw Cookie header string for a specific session.
146
+ *
147
+ * Returns `undefined` if the cookie is not present or the value is not
148
+ * a valid 64-character hex string. When found, the value is normalised
149
+ * to lowercase for deterministic hashing downstream.
150
+ *
151
+ * @param cookieHeader - The raw `Cookie` header value (e.g., "foo=bar; _glide_bind_abc123=...").
152
+ * @param sessionKey - The session key that identifies the cookie to look for.
153
+ * @returns The fe_code value (lowercase), or undefined if the cookie is not present or invalid.
154
+ */
155
+ function parseBindingCookie(cookieHeader, sessionKey) {
156
+ if (!cookieHeader)
157
+ return undefined;
158
+ const cookieName = getBindingCookieName(sessionKey);
159
+ const prefix = `${cookieName}=`;
160
+ const match = cookieHeader.split(';')
161
+ .map(c => c.trim())
162
+ .find(c => c.startsWith(prefix));
163
+ if (!match)
164
+ return undefined;
165
+ const value = match.substring(prefix.length);
166
+ return exports.HEX_64.test(value) ? value.toLowerCase() : undefined;
167
+ }
168
+ /**
169
+ * Builds Set-Cookie headers that expire all previous device binding cookies
170
+ * found in the request.
171
+ *
172
+ * Only one Link flow can be active per browser at a time (App Clip is a
173
+ * single native process), so stale cookies from abandoned flows must be
174
+ * cleared to prevent the old redirect from completing with an outdated
175
+ * binding code.
176
+ *
177
+ * @param cookieHeader - The raw `Cookie` request header.
178
+ * @param options - Cookie options (secure, domain, path) — must match the original set call.
179
+ * @returns Array of Set-Cookie header values that expire stale binding cookies.
180
+ */
181
+ function clearStaleBindingCookies(cookieHeader, options) {
182
+ if (!cookieHeader)
183
+ return [];
184
+ const headers = [];
185
+ for (const pair of cookieHeader.split(';')) {
186
+ const name = pair.trim().split('=')[0];
187
+ if (name.startsWith(exports.BINDING_COOKIE_PREFIX)) {
188
+ const sessionSuffix = name.substring(exports.BINDING_COOKIE_PREFIX.length);
189
+ if (!sessionSuffix)
190
+ continue; // Skip empty suffix (malformed cookie name)
191
+ try {
192
+ headers.push(buildClearBindingCookieHeader(sessionSuffix, options));
193
+ }
194
+ catch (_) {
195
+ // Malformed session suffix (injection chars) — skip silently.
196
+ // Option validation errors (domain/path) also caught here, but since
197
+ // options are caller-provided and identical for every iteration,
198
+ // an invalid option means ALL iterations would throw anyway.
199
+ }
200
+ }
201
+ }
202
+ return headers;
203
+ }
204
+ // ---------------------------------------------------------------------------
205
+ // Completion redirect page
206
+ // ---------------------------------------------------------------------------
207
+ /**
208
+ * Returns the HTML for the completion redirect page.
209
+ *
210
+ * This page extracts `agg_code` and `session_key` from the URL fragment,
211
+ * POSTs them to the developer's backend (which auto-includes the
212
+ * `_glide_bind_{sessionKey}` HttpOnly cookie), and displays the result.
213
+ *
214
+ * On successful completion, a **localStorage signal** is written
215
+ * (`glide_signal_{session_key}`) so the FE SDK in the original tab can
216
+ * detect completion via `StorageEvent` without polling. The signal is
217
+ * automatically cleaned up after 5 seconds.
218
+ *
219
+ * @param completeEndpoint - The developer backend endpoint to POST to (e.g., "/api/glide/complete").
220
+ * @returns Complete HTML string ready to serve.
221
+ */
222
+ function getCompletionPageHtml(completeEndpoint) {
223
+ if (!completeEndpoint.startsWith('/') || completeEndpoint.startsWith('//')) {
224
+ throw new Error('completeEndpoint must be a relative path starting with "/" (e.g., "/api/glide/complete"). ' +
225
+ 'Cross-origin and protocol-relative endpoints (e.g., "//evil.com/...") are not supported because ' +
226
+ 'the page relies on credentials: "include" for cookie-based device binding, which requires same-origin requests.');
227
+ }
228
+ return `<!DOCTYPE html>
229
+ <html lang="en">
230
+ <head>
231
+ <meta charset="utf-8">
232
+ <meta name="viewport" content="width=device-width, initial-scale=1">
233
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'">
234
+ <title>Completing verification...</title>
235
+ <style>
236
+ body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #fafafa; color: #333; }
237
+ .card { text-align: center; padding: 2rem; max-width: 400px; }
238
+ .spinner { width: 40px; height: 40px; border: 3px solid #e0e0e0; border-top-color: #333; border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 1rem; }
239
+ @keyframes spin { to { transform: rotate(360deg); } }
240
+ .hidden { display: none; }
241
+ .error { color: #c62828; }
242
+ .success { color: #2e7d32; }
243
+ .close-hint { margin-top: 0.75rem; font-size: 0.875rem; color: #888; }
244
+ </style>
245
+ </head>
246
+ <body>
247
+ <div class="card">
248
+ <div id="loading" role="status" aria-live="polite">
249
+ <div class="spinner"></div>
250
+ <p>Completing verification&hellip;</p>
251
+ </div>
252
+ <div id="success" class="hidden" role="status" aria-live="polite">
253
+ <p class="success">Verification complete.</p>
254
+ <p id="close-hint" class="close-hint hidden">You may now close this tab.</p>
255
+ </div>
256
+ <div id="error" class="hidden" role="alert" aria-live="assertive">
257
+ <p class="error" id="error-message">Verification failed.</p>
258
+ </div>
259
+ </div>
260
+ <script>
261
+ (async function() {
262
+ var fragment = new URLSearchParams(window.location.hash.substring(1));
263
+ var aggCode = fragment.get('agg_code');
264
+ var sessionKey = fragment.get('session_key');
265
+
266
+ window.history.replaceState(null, '', window.location.pathname + window.location.search);
267
+
268
+ if (!aggCode || !sessionKey) {
269
+ show('error', 'Invalid completion link.');
270
+ return;
271
+ }
272
+
273
+ try {
274
+ var res = await fetch(${JSON.stringify(completeEndpoint)}, {
275
+ method: 'POST',
276
+ credentials: 'include',
277
+ headers: { 'Content-Type': 'application/json' },
278
+ body: JSON.stringify({ agg_code: aggCode, session_key: sessionKey, user_agent: navigator.userAgent })
279
+ });
280
+
281
+ if (res.ok) {
282
+ var signalKey = 'glide_signal_' + sessionKey;
283
+ try {
284
+ localStorage.setItem(signalKey, sessionKey);
285
+ setTimeout(function() {
286
+ try { localStorage.removeItem(signalKey); } catch(e) {}
287
+ }, 5000);
288
+ } catch(e) {}
289
+
290
+ show('success');
291
+ setTimeout(function() { tryClose(); }, 1500);
292
+ } else {
293
+ var errMsg = 'Verification failed. Please try again.';
294
+ try {
295
+ var data = await res.json();
296
+ if (data && data.message) errMsg = data.message;
297
+ } catch (_) {}
298
+ show('error', errMsg);
299
+ }
300
+ } catch (e) {
301
+ show('error', 'Network error. Please try again.');
302
+ }
303
+
304
+ function tryClose() {
305
+ window.close();
306
+ try { window.open('', '_self').close(); } catch(e) {}
307
+ setTimeout(function() {
308
+ document.getElementById('close-hint').classList.remove('hidden');
309
+ }, 200);
310
+ }
311
+
312
+ function show(id, msg) {
313
+ document.getElementById('loading').classList.add('hidden');
314
+ document.getElementById(id).classList.remove('hidden');
315
+ if (msg) document.getElementById('error-message').textContent = msg;
316
+ }
317
+ })();
318
+ </script>
319
+ </body>
320
+ </html>`;
321
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uCAAuC,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAC;AAE3E,OAAO,EAAE,SAAS,EAAE,CAAC;AACrB,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B;;;;;;;;;;;GAWG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACvC,SAAgB,IAAI,sBAAsB;IAC1C,+CAA+C;IAC/C,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,wBAAwB;IACxB,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,gCAAgC;IAChC,SAAgB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnC,SAAgB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnC,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjC,SAAgB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChC,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjC,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAEtC,aAAa,EAAE;QACvB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACrC;IAsBD,kDAAkD;IAClD,EAAE,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO;IAIhC,MAAM;;;;;;;;;;;CAqBT"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uCAAuC,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAC;AAE3E,OAAO,EAAE,SAAS,EAAE,CAAC;AACrB,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B;;;;;;;;;;;GAWG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACvC,SAAgB,IAAI,sBAAsB;IAC1C,+CAA+C;IAC/C,SAAgB,IAAI,EAAE,MAAM,CAAC;IAC7B,wBAAwB;IACxB,SAAgB,MAAM,EAAE,MAAM,CAAC;IAC/B,gCAAgC;IAChC,SAAgB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnC,SAAgB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnC,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjC,SAAgB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChC,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjC,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAEtC,aAAa,EAAE;QACvB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACrC;IAsBD,kDAAkD;IAClD,EAAE,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO;IAIhC,MAAM;;;;;;;;;;;CAaT"}
package/dist/errors.js CHANGED
@@ -52,14 +52,5 @@ class MagicalAuthError extends Error {
52
52
  details: this.details
53
53
  };
54
54
  }
55
- /** Custom inspect for cleaner console output (Node.js). */
56
- [Symbol.for('nodejs.util.inspect.custom')]() {
57
- const props = [`code: '${this.code}'`, `status: ${this.status}`];
58
- if (this.requestId)
59
- props.push(`requestId: '${this.requestId}'`);
60
- if (this.details)
61
- props.push(`details: ${JSON.stringify(this.details)}`);
62
- return `MagicalAuthError: ${this.message} { ${props.join(', ')} }`;
63
- }
64
55
  }
65
56
  exports.MagicalAuthError = MagicalAuthError;
package/dist/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  export * from '@glideidentity/glide-be-sdk-node-core';
2
- export type { GlideSdkSettings, LogFormat, TokenResponse, CachedToken } from './types';
2
+ export type { GlideSdkSettings, LogFormat, TokenResponse, CachedToken, PrepareResult } from './types';
3
3
  export { GlideClient } from './glide';
4
4
  export type { Logger, LogField } from './logger';
5
5
  export { LogLevel, createLogger, generateRequestId } from './logger';
6
6
  export { MagicalAuthError } from './errors';
7
7
  export { validatePhoneNumber, validatePlmn, validateUseCaseRequirements } from './validation';
8
8
  export type { ValidationResult } from './validation';
9
+ export { BINDING_COOKIE_PREFIX, BINDING_COOKIE_MAX_AGE, getBindingCookieName, HEX_64, assertValidFeCode, generateFeCode, computeFeHash, buildSetBindingCookieHeader, buildClearBindingCookieHeader, parseBindingCookie, clearStaleBindingCookies, getCompletionPageHtml, } from './device-binding';
10
+ export type { BindingCookieOptions } from './device-binding';
9
11
  export { request, FetchError } from './internal/http';
10
12
  export type { RequestOptions, HttpResponse } from './internal/http';
11
13
  export { getStatusUrl } from './utils';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,uCAAuC,CAAC;AAGtD,YAAY,EAAE,gBAAgB,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGvF,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGtC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAGrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAG5C,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAC9F,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,uCAAuC,CAAC;AAGtD,YAAY,EAAE,gBAAgB,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGtG,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGtC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAGrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAG5C,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAC9F,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,OAAO,EACH,qBAAqB,EACrB,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,2BAA2B,EAC3B,6BAA6B,EAC7B,kBAAkB,EAClB,wBAAwB,EACxB,qBAAqB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAG7D,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAGpE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.getStatusUrl = exports.FetchError = exports.request = exports.validateUseCaseRequirements = exports.validatePlmn = exports.validatePhoneNumber = exports.MagicalAuthError = exports.generateRequestId = exports.createLogger = exports.LogLevel = exports.GlideClient = void 0;
17
+ exports.getStatusUrl = exports.FetchError = exports.request = exports.getCompletionPageHtml = exports.clearStaleBindingCookies = exports.parseBindingCookie = exports.buildClearBindingCookieHeader = exports.buildSetBindingCookieHeader = exports.computeFeHash = exports.generateFeCode = exports.assertValidFeCode = exports.HEX_64 = exports.getBindingCookieName = exports.BINDING_COOKIE_MAX_AGE = exports.BINDING_COOKIE_PREFIX = exports.validateUseCaseRequirements = exports.validatePlmn = exports.validatePhoneNumber = exports.MagicalAuthError = exports.generateRequestId = exports.createLogger = exports.LogLevel = exports.GlideClient = void 0;
18
18
  // Re-export all core types and constants (single source of truth)
19
19
  __exportStar(require("@glideidentity/glide-be-sdk-node-core"), exports);
20
20
  // Main client
@@ -32,6 +32,20 @@ var validation_1 = require("./validation");
32
32
  Object.defineProperty(exports, "validatePhoneNumber", { enumerable: true, get: function () { return validation_1.validatePhoneNumber; } });
33
33
  Object.defineProperty(exports, "validatePlmn", { enumerable: true, get: function () { return validation_1.validatePlmn; } });
34
34
  Object.defineProperty(exports, "validateUseCaseRequirements", { enumerable: true, get: function () { return validation_1.validateUseCaseRequirements; } });
35
+ // Device binding utilities
36
+ var device_binding_1 = require("./device-binding");
37
+ Object.defineProperty(exports, "BINDING_COOKIE_PREFIX", { enumerable: true, get: function () { return device_binding_1.BINDING_COOKIE_PREFIX; } });
38
+ Object.defineProperty(exports, "BINDING_COOKIE_MAX_AGE", { enumerable: true, get: function () { return device_binding_1.BINDING_COOKIE_MAX_AGE; } });
39
+ Object.defineProperty(exports, "getBindingCookieName", { enumerable: true, get: function () { return device_binding_1.getBindingCookieName; } });
40
+ Object.defineProperty(exports, "HEX_64", { enumerable: true, get: function () { return device_binding_1.HEX_64; } });
41
+ Object.defineProperty(exports, "assertValidFeCode", { enumerable: true, get: function () { return device_binding_1.assertValidFeCode; } });
42
+ Object.defineProperty(exports, "generateFeCode", { enumerable: true, get: function () { return device_binding_1.generateFeCode; } });
43
+ Object.defineProperty(exports, "computeFeHash", { enumerable: true, get: function () { return device_binding_1.computeFeHash; } });
44
+ Object.defineProperty(exports, "buildSetBindingCookieHeader", { enumerable: true, get: function () { return device_binding_1.buildSetBindingCookieHeader; } });
45
+ Object.defineProperty(exports, "buildClearBindingCookieHeader", { enumerable: true, get: function () { return device_binding_1.buildClearBindingCookieHeader; } });
46
+ Object.defineProperty(exports, "parseBindingCookie", { enumerable: true, get: function () { return device_binding_1.parseBindingCookie; } });
47
+ Object.defineProperty(exports, "clearStaleBindingCookies", { enumerable: true, get: function () { return device_binding_1.clearStaleBindingCookies; } });
48
+ Object.defineProperty(exports, "getCompletionPageHtml", { enumerable: true, get: function () { return device_binding_1.getCompletionPageHtml; } });
35
49
  // HTTP utilities - for advanced usage and dev-server
36
50
  var http_1 = require("./internal/http");
37
51
  Object.defineProperty(exports, "request", { enumerable: true, get: function () { return http_1.request; } });
@@ -1 +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"}
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;AAmHD;;;;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 CHANGED
@@ -13,7 +13,9 @@ var LogLevel;
13
13
  })(LogLevel || (exports.LogLevel = LogLevel = {}));
14
14
  const SENSITIVE_KEYS = [
15
15
  'apikey', 'api_key', 'api-key', 'token', 'password', 'secret',
16
- 'credential', 'authorization', 'bearer', 'key'
16
+ 'credential', 'authorization', 'bearer', 'key',
17
+ 'fe_code', 'fecode', 'fe_hash', 'fehash', 'agg_code', 'aggcode',
18
+ '_glide_bind', 'glide_bind'
17
19
  ];
18
20
  const SENSITIVE_PATTERN = new RegExp(`(${SENSITIVE_KEYS.join('|')})([=:\\s"']+)([^\\s"'&,}{\\]\\)]+)`, 'gi');
19
21
  function sanitizeMessage(message) {
@@ -1,15 +1,22 @@
1
- import { GlideSdkSettings } from '../types';
1
+ import { GlideSdkSettings, PrepareResult } from '../types';
2
2
  import { Logger } from '../logger';
3
3
  import { OAuthManager } from '../internal/oauth';
4
- import type { PrepareRequest, PrepareResponse, VerifyPhoneNumberRequest, VerifyPhoneNumberResponse, GetPhoneNumberRequest, GetPhoneNumberResponse, ReportInvocationRequest, ReportInvocationResponse } from '@glideidentity/glide-be-sdk-node-core';
4
+ import type { PrepareRequest, CompleteRequest, VerifyPhoneNumberRequest, VerifyPhoneNumberResponse, GetPhoneNumberRequest, GetPhoneNumberResponse, ReportInvocationRequest, ReportInvocationResponse, StatusResponse } from '@glideidentity/glide-be-sdk-node-core';
5
5
  /** Handles SIM-based phone authentication via the Glide API. */
6
6
  export declare class MagicalAuth {
7
7
  private settings;
8
8
  private oauthClient;
9
9
  private logger;
10
10
  constructor(settings: GlideSdkSettings, oauthClient: OAuthManager, logger: Logger);
11
- /** Initiates a phone authentication session. */
12
- prepare(req: PrepareRequest): Promise<PrepareResponse>;
11
+ /**
12
+ * Initiates a phone authentication session.
13
+ *
14
+ * For link protocol sessions, this method automatically generates a device
15
+ * binding code (`feCode`) and includes its hash in the aggregator request.
16
+ * When `feCode` is present in the result, the caller MUST set it as an
17
+ * HttpOnly cookie — use `buildSetBindingCookieHeader()` from `device-binding`.
18
+ */
19
+ prepare(req: PrepareRequest): Promise<PrepareResult>;
13
20
  /** Verifies a phone number matches the device. */
14
21
  verifyPhoneNumber(req: VerifyPhoneNumberRequest): Promise<VerifyPhoneNumberResponse>;
15
22
  /** Retrieves the phone number. */
@@ -27,6 +34,28 @@ export declare class MagicalAuth {
27
34
  * @returns Response with success status
28
35
  */
29
36
  reportInvocation(req: ReportInvocationRequest): Promise<ReportInvocationResponse>;
37
+ /**
38
+ * Completes a device-bound authentication session.
39
+ *
40
+ * Call this after the completion redirect page POSTs `agg_code` and `session_key`
41
+ * to the developer backend. The `fe_code` should be read from the `_glide_bind`
42
+ * HttpOnly cookie — use `parseBindingCookie()` from `device-binding`.
43
+ *
44
+ * The complete endpoint does not return verification results. The client-side
45
+ * poller detects the session status change to `completed` and calls `/process`.
46
+ *
47
+ * @param req - The complete request containing session_key, fe_code, and agg_code.
48
+ */
49
+ complete(req: CompleteRequest): Promise<void>;
50
+ /**
51
+ * Checks the status of an authentication session.
52
+ *
53
+ * Use this to poll for completion of async flows (link/desktop).
54
+ * This is the authenticated version — requires a Bearer token.
55
+ *
56
+ * @param sessionKey - The 16-character hex session key from the prepare response.
57
+ */
58
+ checkStatus(sessionKey: string): Promise<StatusResponse>;
30
59
  private handleError;
31
60
  }
32
61
  //# sourceMappingURL=magical-auth.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"magical-auth.d.ts","sourceRoot":"","sources":["../../src/services/magical-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnC,OAAO,EAAE,YAAY,EAAc,MAAM,mBAAmB,CAAC;AAK7D,OAAO,KAAK,EACR,cAAc,EAEd,eAAe,EACf,wBAAwB,EACxB,yBAAyB,EACzB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,EACjB,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;;;;;;;OAOG;IACG,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAkG1D,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;;;;;;;;;;;OAWG;IACG,QAAQ,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAiEnD;;;;;;;OAOG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAgC9D,OAAO,CAAC,WAAW;CAyCtB"}
@@ -5,6 +5,8 @@ const http_1 = require("../internal/http");
5
5
  const oauth_1 = require("../internal/oauth");
6
6
  const errors_1 = require("../errors");
7
7
  const validation_1 = require("../validation");
8
+ const device_binding_1 = require("../device-binding");
9
+ const glide_be_sdk_node_core_1 = require("@glideidentity/glide-be-sdk-node-core");
8
10
  function generateNonce(length = 32) {
9
11
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
10
12
  let result = '';
@@ -22,7 +24,14 @@ class MagicalAuth {
22
24
  this.oauthClient = oauthClient;
23
25
  this.logger = logger;
24
26
  }
25
- /** Initiates a phone authentication session. */
27
+ /**
28
+ * Initiates a phone authentication session.
29
+ *
30
+ * For link protocol sessions, this method automatically generates a device
31
+ * binding code (`feCode`) and includes its hash in the aggregator request.
32
+ * When `feCode` is present in the result, the caller MUST set it as an
33
+ * HttpOnly cookie — use `buildSetBindingCookieHeader()` from `device-binding`.
34
+ */
26
35
  async prepare(req) {
27
36
  const hasParentSession = req.options?.parent_session_id;
28
37
  if (!hasParentSession) {
@@ -31,8 +40,7 @@ class MagicalAuth {
31
40
  throw new errors_1.MagicalAuthError({
32
41
  code: useCaseValidation.errorCode || errors_1.ErrorCode.INVALID_USE_CASE,
33
42
  message: useCaseValidation.error,
34
- status: 400,
35
- details: useCaseValidation.details
43
+ status: 400
36
44
  });
37
45
  }
38
46
  }
@@ -56,24 +64,29 @@ class MagicalAuth {
56
64
  });
57
65
  }
58
66
  }
67
+ // Generate device binding code and hash for potential link protocol sessions.
68
+ // Always sent — the aggregator ignores it for non-link strategies (TS43, desktop).
69
+ const feCode = (0, device_binding_1.generateFeCode)();
70
+ const feHash = (0, device_binding_1.computeFeHash)(feCode);
59
71
  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;
72
+ // Only include optional fields when truthy to avoid sending null / ""
73
+ // to the aggregator — JSON.stringify serializes null and "" but the
74
+ // aggregator may reject or misinterpret them.
75
+ const body = {
76
+ nonce,
77
+ fe_hash: feHash,
78
+ ...(req.use_case ? { use_case: req.use_case } : undefined),
79
+ ...(req.phone_number ? { phone_number: req.phone_number } : undefined),
80
+ ...(req.plmn ? { plmn: req.plmn } : undefined),
81
+ ...(req.consent_data ? { consent_data: req.consent_data } : undefined),
82
+ ...(req.options ? { options: req.options } : undefined),
83
+ };
84
+ // Always include client_info so the aggregator can compare the
85
+ // browser user-agent between /prepare and /complete (browser mismatch detection).
86
+ body.client_info = {
87
+ user_agent: req.client_info?.user_agent || 'unknown',
88
+ platform: req.client_info?.platform || 'unknown'
89
+ };
77
90
  const url = `${this.settings.baseUrl}/magic-auth/v2/auth/prepare`;
78
91
  this.logger.debug('Prepare request', { url });
79
92
  try {
@@ -89,7 +102,12 @@ class MagicalAuth {
89
102
  });
90
103
  const result = response.json();
91
104
  this.logger.debug('Prepare completed', { strategy: result.authentication_strategy });
92
- return result;
105
+ // Only expose feCode when the session uses link protocol (device binding).
106
+ const prepareResult = { ...result };
107
+ if (result.authentication_strategy === glide_be_sdk_node_core_1.AuthenticationStrategy.LINK) {
108
+ prepareResult.feCode = feCode;
109
+ }
110
+ return prepareResult;
93
111
  }
94
112
  catch (error) {
95
113
  this.handleError(error);
@@ -214,7 +232,112 @@ class MagicalAuth {
214
232
  body: JSON.stringify(body),
215
233
  });
216
234
  const result = await response.json();
217
- this.logger.debug('ReportInvocation completed', { session_id: req.session_id, success: result.success });
235
+ this.logger.debug('ReportInvocation completed', { session_id: req.session_id, status: result.status });
236
+ return result;
237
+ }
238
+ catch (error) {
239
+ this.handleError(error);
240
+ }
241
+ }
242
+ /**
243
+ * Completes a device-bound authentication session.
244
+ *
245
+ * Call this after the completion redirect page POSTs `agg_code` and `session_key`
246
+ * to the developer backend. The `fe_code` should be read from the `_glide_bind`
247
+ * HttpOnly cookie — use `parseBindingCookie()` from `device-binding`.
248
+ *
249
+ * The complete endpoint does not return verification results. The client-side
250
+ * poller detects the session status change to `completed` and calls `/process`.
251
+ *
252
+ * @param req - The complete request containing session_key, fe_code, and agg_code.
253
+ */
254
+ async complete(req) {
255
+ if (!req.session_key) {
256
+ throw new errors_1.MagicalAuthError({
257
+ code: errors_1.ErrorCode.MISSING_REQUIRED_FIELD,
258
+ message: 'session_key is required',
259
+ status: 400
260
+ });
261
+ }
262
+ if (!req.fe_code) {
263
+ throw new errors_1.MagicalAuthError({
264
+ code: errors_1.ErrorCode.MISSING_BINDING_COOKIE,
265
+ message: 'fe_code is required (from _glide_bind cookie)',
266
+ status: 403
267
+ });
268
+ }
269
+ if (!req.agg_code) {
270
+ throw new errors_1.MagicalAuthError({
271
+ code: errors_1.ErrorCode.MISSING_REQUIRED_FIELD,
272
+ message: 'agg_code is required',
273
+ status: 400
274
+ });
275
+ }
276
+ // Validate format — both codes must be 64-char lowercase hex.
277
+ // Use a generic 403 to avoid leaking which value failed.
278
+ if (!device_binding_1.HEX_64.test(req.fe_code) || !device_binding_1.HEX_64.test(req.agg_code)) {
279
+ throw new errors_1.MagicalAuthError({
280
+ code: errors_1.ErrorCode.DEVICE_BINDING_FAILED,
281
+ message: 'Invalid device binding inputs',
282
+ status: 403
283
+ });
284
+ }
285
+ const body = {
286
+ session_key: req.session_key,
287
+ fe_code: req.fe_code,
288
+ agg_code: req.agg_code,
289
+ };
290
+ if (req.user_agent) {
291
+ body.user_agent = req.user_agent;
292
+ }
293
+ const url = `${this.settings.baseUrl}/magic-auth/v2/auth/complete`;
294
+ this.logger.debug('Complete request', { url });
295
+ try {
296
+ const accessToken = await this.oauthClient.getAccessToken();
297
+ await (0, http_1.request)(url, {
298
+ method: 'POST',
299
+ headers: {
300
+ 'Content-Type': 'application/json',
301
+ 'Accept': 'application/json',
302
+ 'Authorization': `Bearer ${accessToken}`,
303
+ },
304
+ body: JSON.stringify(body),
305
+ });
306
+ this.logger.debug('Complete succeeded');
307
+ }
308
+ catch (error) {
309
+ this.handleError(error);
310
+ }
311
+ }
312
+ /**
313
+ * Checks the status of an authentication session.
314
+ *
315
+ * Use this to poll for completion of async flows (link/desktop).
316
+ * This is the authenticated version — requires a Bearer token.
317
+ *
318
+ * @param sessionKey - The 16-character hex session key from the prepare response.
319
+ */
320
+ async checkStatus(sessionKey) {
321
+ if (!sessionKey) {
322
+ throw new errors_1.MagicalAuthError({
323
+ code: errors_1.ErrorCode.MISSING_REQUIRED_FIELD,
324
+ message: 'sessionKey is required',
325
+ status: 400
326
+ });
327
+ }
328
+ const url = `${this.settings.baseUrl}/magic-auth/v2/auth/status/${sessionKey}`;
329
+ this.logger.debug('CheckStatus request', { url, sessionKey });
330
+ try {
331
+ const accessToken = await this.oauthClient.getAccessToken();
332
+ const response = await (0, http_1.request)(url, {
333
+ method: 'GET',
334
+ headers: {
335
+ 'Accept': 'application/json',
336
+ 'Authorization': `Bearer ${accessToken}`,
337
+ },
338
+ });
339
+ const result = response.json();
340
+ this.logger.debug('CheckStatus completed', { sessionKey, status: result.status });
218
341
  return result;
219
342
  }
220
343
  catch (error) {
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Logger, LogLevel } from './logger';
2
+ import type { PrepareResponse } from '@glideidentity/glide-be-sdk-node-core';
2
3
  /** Log output format. */
3
4
  export type LogFormat = 'json' | 'text';
4
5
  /** OAuth2 token response from the token endpoint. */
@@ -19,6 +20,24 @@ export interface CachedToken {
19
20
  /** Timestamp (ms) when the token expires. */
20
21
  expiresAt: number;
21
22
  }
23
+ /**
24
+ * Extended prepare response that includes the device binding code for link protocol.
25
+ *
26
+ * Compatible with `PrepareResponse` — existing code that expects `PrepareResponse` will
27
+ * continue to work unchanged. The `feCode` field is only present when the session uses
28
+ * the link authentication strategy.
29
+ */
30
+ export interface PrepareResult extends PrepareResponse {
31
+ /**
32
+ * Device binding code for link protocol sessions.
33
+ *
34
+ * When present, the caller MUST set this as an HttpOnly cookie using
35
+ * `buildSetBindingCookieHeader(feCode)` from `device-binding.ts`.
36
+ *
37
+ * `undefined` for non-link strategies (TS43, desktop).
38
+ */
39
+ feCode?: string;
40
+ }
22
41
  /** Client configuration options. */
23
42
  export interface GlideSdkSettings {
24
43
  /** OAuth2 Client ID (required). */
@@ -1 +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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AAE7E,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;;;;;;GAMG;AACH,MAAM,WAAW,aAAc,SAAQ,eAAe;IAClD;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;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"}
@@ -4,7 +4,6 @@ export interface ValidationResult {
4
4
  valid: boolean;
5
5
  error?: string;
6
6
  errorCode?: string;
7
- details?: Record<string, unknown>;
8
7
  }
9
8
  /** Validates phone number format (E.164). */
10
9
  export declare function validatePhoneNumber(phoneNumber?: string): ValidationResult;
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAC;AAE/E,yBAAyB;AACzB,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED,6CAA6C;AAC7C,wBAAgB,mBAAmB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAyC1E;AAED,uCAAuC;AACvC,wBAAgB,YAAY,CAAC,IAAI,CAAC,EAAE,IAAI,GAAG,gBAAgB,CAoB1D;AAED,+EAA+E;AAC/E,wBAAgB,2BAA2B,CACvC,OAAO,CAAC,EAAE,WAAW,EACrB,WAAW,CAAC,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,IAAI,GACZ,gBAAgB,CA0ClB"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAC;AAE/E,yBAAyB;AACzB,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,6CAA6C;AAC7C,wBAAgB,mBAAmB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAyC1E;AAED,uCAAuC;AACvC,wBAAgB,YAAY,CAAC,IAAI,CAAC,EAAE,IAAI,GAAG,gBAAgB,CAoB1D;AAED,+EAA+E;AAC/E,wBAAgB,2BAA2B,CACvC,OAAO,CAAC,EAAE,WAAW,EACrB,WAAW,CAAC,EAAE,MAAM,EACpB,IAAI,CAAC,EAAE,IAAI,GACZ,gBAAgB,CAuClB"}
@@ -75,16 +75,14 @@ function validateUseCaseRequirements(useCase, phoneNumber, plmn) {
75
75
  return {
76
76
  valid: false,
77
77
  error: 'Phone number should not be provided for GetPhoneNumber use case',
78
- errorCode: glide_be_sdk_node_core_1.ErrorCode.INVALID_USE_CASE,
79
- details: { invalid_field: 'phone_number', reason: 'not_allowed_for_use_case' }
78
+ errorCode: glide_be_sdk_node_core_1.ErrorCode.INVALID_USE_CASE
80
79
  };
81
80
  }
82
81
  if (!plmn) {
83
82
  return {
84
83
  valid: false,
85
84
  error: 'PLMN (MCC/MNC) is required for GetPhoneNumber use case',
86
- errorCode: glide_be_sdk_node_core_1.ErrorCode.MISSING_REQUIRED_FIELD,
87
- details: { missing_field: 'plmn' }
85
+ errorCode: glide_be_sdk_node_core_1.ErrorCode.MISSING_REQUIRED_FIELD
88
86
  };
89
87
  }
90
88
  break;
@@ -93,8 +91,7 @@ function validateUseCaseRequirements(useCase, phoneNumber, plmn) {
93
91
  return {
94
92
  valid: false,
95
93
  error: 'Phone number is required for VerifyPhoneNumber use case',
96
- errorCode: glide_be_sdk_node_core_1.ErrorCode.MISSING_REQUIRED_FIELD,
97
- details: { missing_field: 'phone_number' }
94
+ errorCode: glide_be_sdk_node_core_1.ErrorCode.MISSING_REQUIRED_FIELD
98
95
  };
99
96
  }
100
97
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glideidentity/glide-be-sdk-node",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Glide SDK for Node.js - carrier-based phone verification",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,8 +21,17 @@
21
21
  "clean": "rm -rf dist"
22
22
  },
23
23
  "dependencies": {
24
- "@glideidentity/glide-be-sdk-node-core": "2.0.1"
24
+ "@glideidentity/glide-be-sdk-node-core": "2.1.0"
25
25
  },
26
- "keywords": ["glide", "sdk", "phone", "verification", "magic-auth"],
26
+ "publishConfig": {
27
+ "registry": "https://us-central1-npm.pkg.dev/platform-devops-09ff133a/npm-packages/"
28
+ },
29
+ "keywords": [
30
+ "glide",
31
+ "sdk",
32
+ "phone",
33
+ "verification",
34
+ "magic-auth"
35
+ ],
27
36
  "license": "SEE LICENSE IN LICENSE"
28
37
  }