@pelican-identity/auth-core 1.2.9 → 1.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -13
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -1
- package/dist/constants.js.map +1 -1
- package/dist/engine/engine.d.ts +12 -5
- package/dist/engine/engine.d.ts.map +1 -1
- package/dist/engine/engine.js +123 -76
- package/dist/engine/engine.js.map +1 -1
- package/dist/index.d.mts +17 -6
- package/dist/index.d.ts +198 -8
- package/dist/index.js +613 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +222 -124
- package/dist/index.mjs.map +1 -1
- package/dist/utilities/transport.d.ts +4 -0
- package/dist/utilities/transport.d.ts.map +1 -1
- package/dist/utilities/transport.js +65 -11
- package/dist/utilities/transport.js.map +1 -1
- package/package.json +1 -1
- package/dist/types/types.d.ts +0 -102
- package/dist/types/types.d.ts.map +0 -1
- package/dist/types/types.js +0 -2
- package/dist/types/types.js.map +0 -1
- package/dist/utilities/crypto.d.ts +0 -18
- package/dist/utilities/crypto.d.ts.map +0 -1
- package/dist/utilities/crypto.js +0 -37
- package/dist/utilities/crypto.js.map +0 -1
- package/dist/utilities/stateMachine.d.ts +0 -9
- package/dist/utilities/stateMachine.d.ts.map +0 -1
- package/dist/utilities/stateMachine.js +0 -18
- package/dist/utilities/stateMachine.js.map +0 -1
- package/dist/utilities/storage.d.ts +0 -26
- package/dist/utilities/storage.d.ts.map +0 -1
- package/dist/utilities/storage.js +0 -92
- package/dist/utilities/storage.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,614 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var nacl = require('tweetnacl');
|
|
4
|
+
var tweetnaclUtil = require('tweetnacl-util');
|
|
5
|
+
var QRCode = require('qrcode');
|
|
6
|
+
|
|
7
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var nacl__default = /*#__PURE__*/_interopDefault(nacl);
|
|
10
|
+
var QRCode__default = /*#__PURE__*/_interopDefault(QRCode);
|
|
11
|
+
|
|
12
|
+
// src/utilities/crypto.ts
|
|
13
|
+
var CryptoService = class {
|
|
14
|
+
generateSymmetricKey() {
|
|
15
|
+
const key = nacl__default.default.randomBytes(32);
|
|
16
|
+
return tweetnaclUtil.encodeBase64(key);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Encrypt with symmetric key (secret-key encryption)
|
|
20
|
+
* Use this when both parties share the same secret key
|
|
21
|
+
* @param plaintext - Message to encrypt
|
|
22
|
+
* @param keyString - Symmetric key (base64)
|
|
23
|
+
* @returns Encrypted message with nonce
|
|
24
|
+
*/
|
|
25
|
+
encryptSymmetric({
|
|
26
|
+
plaintext,
|
|
27
|
+
keyString
|
|
28
|
+
}) {
|
|
29
|
+
const key = tweetnaclUtil.decodeBase64(keyString);
|
|
30
|
+
const nonce = nacl__default.default.randomBytes(24);
|
|
31
|
+
const messageBytes = new TextEncoder().encode(plaintext);
|
|
32
|
+
const ciphertext = nacl__default.default.secretbox(messageBytes, nonce, key);
|
|
33
|
+
return {
|
|
34
|
+
cipher: tweetnaclUtil.encodeBase64(ciphertext),
|
|
35
|
+
nonce: tweetnaclUtil.encodeBase64(nonce)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Decrypt with symmetric key
|
|
40
|
+
* @param encrypted - Encrypted message with nonce
|
|
41
|
+
* @param keyString - Symmetric key (base64)
|
|
42
|
+
* @returns Decrypted plaintext
|
|
43
|
+
*/
|
|
44
|
+
decryptSymmetric({
|
|
45
|
+
encrypted,
|
|
46
|
+
keyString
|
|
47
|
+
}) {
|
|
48
|
+
try {
|
|
49
|
+
const key = tweetnaclUtil.decodeBase64(keyString);
|
|
50
|
+
const ciphertextBytes = tweetnaclUtil.decodeBase64(encrypted.cipher);
|
|
51
|
+
const nonceBytes = tweetnaclUtil.decodeBase64(encrypted.nonce);
|
|
52
|
+
const decrypted = nacl__default.default.secretbox.open(ciphertextBytes, nonceBytes, key);
|
|
53
|
+
if (!decrypted) {
|
|
54
|
+
throw new Error("Decryption failed - invalid key or corrupted data");
|
|
55
|
+
}
|
|
56
|
+
const decoded = new TextDecoder().decode(decrypted);
|
|
57
|
+
return decoded;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("Decryption failed", error);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var crypto_default = CryptoService;
|
|
65
|
+
|
|
66
|
+
// src/utilities/storage.ts
|
|
67
|
+
var AuthStorage = class {
|
|
68
|
+
constructor() {
|
|
69
|
+
this.prefix = "pelican_auth_";
|
|
70
|
+
this.defaultTTL = 5 * 60 * 1e3;
|
|
71
|
+
// 5 minutes
|
|
72
|
+
this.memoryCache = /* @__PURE__ */ new Map();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Store auth session with automatic cleanup
|
|
76
|
+
*/
|
|
77
|
+
set(key, value, options = {}) {
|
|
78
|
+
const { ttlMs = this.defaultTTL, useSessionStorage = true } = options;
|
|
79
|
+
const expiresAt = Date.now() + ttlMs;
|
|
80
|
+
const data = { value, expiresAt };
|
|
81
|
+
if (useSessionStorage) {
|
|
82
|
+
try {
|
|
83
|
+
sessionStorage.setItem(`${this.prefix}${key}`, JSON.stringify(data));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.warn("SessionStorage unavailable, using memory:", error);
|
|
86
|
+
this.setMemory(key, data);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
this.setMemory(key, data);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get stored value if not expired
|
|
94
|
+
*/
|
|
95
|
+
get(key) {
|
|
96
|
+
try {
|
|
97
|
+
const stored = sessionStorage.getItem(`${this.prefix}${key}`);
|
|
98
|
+
if (stored) {
|
|
99
|
+
const data = JSON.parse(stored);
|
|
100
|
+
if (Date.now() > data.expiresAt) {
|
|
101
|
+
this.remove(key);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return data.value;
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
}
|
|
108
|
+
return this.getMemory(key);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Remove specific key
|
|
112
|
+
*/
|
|
113
|
+
remove(key) {
|
|
114
|
+
try {
|
|
115
|
+
sessionStorage.removeItem(`${this.prefix}${key}`);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
}
|
|
118
|
+
this.memoryCache.delete(key);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Clear all auth data
|
|
122
|
+
*/
|
|
123
|
+
clear() {
|
|
124
|
+
try {
|
|
125
|
+
Object.keys(sessionStorage).forEach((key) => {
|
|
126
|
+
if (key.startsWith(this.prefix)) {
|
|
127
|
+
sessionStorage.removeItem(key);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
}
|
|
132
|
+
this.memoryCache.clear();
|
|
133
|
+
}
|
|
134
|
+
// ========================================
|
|
135
|
+
// Memory cache helpers
|
|
136
|
+
// ========================================
|
|
137
|
+
setMemory(key, data) {
|
|
138
|
+
this.memoryCache.set(key, data);
|
|
139
|
+
const ttl = data.expiresAt - Date.now();
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
this.memoryCache.delete(key);
|
|
142
|
+
}, ttl);
|
|
143
|
+
}
|
|
144
|
+
getMemory(key) {
|
|
145
|
+
const data = this.memoryCache.get(key);
|
|
146
|
+
if (!data) return null;
|
|
147
|
+
if (Date.now() > data.expiresAt) {
|
|
148
|
+
this.memoryCache.delete(key);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return data.value;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var storage = new AuthStorage();
|
|
155
|
+
var storeAuthSession = (sessionId, sessionKey, ttlMs) => {
|
|
156
|
+
storage.set("session", { sessionId, sessionKey }, { ttlMs });
|
|
157
|
+
};
|
|
158
|
+
var getAuthSession = () => {
|
|
159
|
+
return storage.get("session");
|
|
160
|
+
};
|
|
161
|
+
var clearAuthSession = () => {
|
|
162
|
+
storage.remove("session");
|
|
163
|
+
};
|
|
164
|
+
var clearAllAuthData = () => {
|
|
165
|
+
storage.clear();
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// src/utilities/stateMachine.ts
|
|
169
|
+
var StateMachine = class {
|
|
170
|
+
constructor() {
|
|
171
|
+
this.state = "idle";
|
|
172
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
173
|
+
}
|
|
174
|
+
get current() {
|
|
175
|
+
return this.state;
|
|
176
|
+
}
|
|
177
|
+
transition(next) {
|
|
178
|
+
this.state = next;
|
|
179
|
+
this.listeners.forEach((l) => l(next));
|
|
180
|
+
}
|
|
181
|
+
subscribe(fn) {
|
|
182
|
+
this.listeners.add(fn);
|
|
183
|
+
return () => this.listeners.delete(fn);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/utilities/transport.ts
|
|
188
|
+
var Transport = class {
|
|
189
|
+
constructor(handlers) {
|
|
190
|
+
this.reconnectAttempts = 0;
|
|
191
|
+
this.maxReconnectAttempts = 3;
|
|
192
|
+
// Reduced from 5 for mobile
|
|
193
|
+
this.isExplicitlyClosed = false;
|
|
194
|
+
this.isReconnecting = false;
|
|
195
|
+
this.handlers = handlers;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Establish WebSocket connection
|
|
199
|
+
* @param url - WebSocket URL
|
|
200
|
+
*/
|
|
201
|
+
connect(url) {
|
|
202
|
+
this.url = url;
|
|
203
|
+
this.isExplicitlyClosed = false;
|
|
204
|
+
if (this.socket) {
|
|
205
|
+
this.socket.onclose = null;
|
|
206
|
+
this.socket.onerror = null;
|
|
207
|
+
this.socket.onmessage = null;
|
|
208
|
+
this.socket.onopen = null;
|
|
209
|
+
if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
|
|
210
|
+
this.socket.close();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
this.socket = new WebSocket(url);
|
|
214
|
+
this.socket.onopen = () => {
|
|
215
|
+
this.reconnectAttempts = 0;
|
|
216
|
+
this.isReconnecting = false;
|
|
217
|
+
this.handlers.onOpen?.();
|
|
218
|
+
};
|
|
219
|
+
this.socket.onmessage = (e) => {
|
|
220
|
+
try {
|
|
221
|
+
const data = JSON.parse(e.data);
|
|
222
|
+
this.handlers.onMessage?.(data);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error("Failed to parse WebSocket message", err);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
this.socket.onerror = (e) => {
|
|
228
|
+
if (!this.isReconnecting && !this.isExplicitlyClosed) {
|
|
229
|
+
this.handlers.onError?.(e);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
this.socket.onclose = (e) => {
|
|
233
|
+
if (this.reconnectTimeout) {
|
|
234
|
+
clearTimeout(this.reconnectTimeout);
|
|
235
|
+
this.reconnectTimeout = void 0;
|
|
236
|
+
}
|
|
237
|
+
if (!this.isReconnecting) {
|
|
238
|
+
this.handlers.onClose?.(e);
|
|
239
|
+
}
|
|
240
|
+
if (!this.isExplicitlyClosed && !this.isReconnecting && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
241
|
+
this.attemptReconnect();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Attempt to reconnect with exponential backoff
|
|
247
|
+
*/
|
|
248
|
+
attemptReconnect() {
|
|
249
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
250
|
+
console.warn("Max WebSocket reconnect attempts reached");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
this.isReconnecting = true;
|
|
254
|
+
this.reconnectAttempts++;
|
|
255
|
+
const delay = Math.min(Math.pow(2, this.reconnectAttempts - 1) * 500, 2e3);
|
|
256
|
+
console.log(
|
|
257
|
+
`Reconnecting WebSocket (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}) in ${delay}ms...`
|
|
258
|
+
);
|
|
259
|
+
this.reconnectTimeout = window.setTimeout(() => {
|
|
260
|
+
if (this.url && !this.isExplicitlyClosed) {
|
|
261
|
+
this.connect(this.url);
|
|
262
|
+
}
|
|
263
|
+
}, delay);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Send a message through the WebSocket
|
|
267
|
+
* Queues message if connection is not open
|
|
268
|
+
*/
|
|
269
|
+
send(payload) {
|
|
270
|
+
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
271
|
+
try {
|
|
272
|
+
this.socket.send(JSON.stringify(payload));
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.error("Failed to send WebSocket message:", err);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
console.warn(
|
|
278
|
+
"WebSocket not open, message not sent:",
|
|
279
|
+
this.socket?.readyState
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Close the WebSocket connection
|
|
285
|
+
* Prevents automatic reconnection
|
|
286
|
+
*/
|
|
287
|
+
close() {
|
|
288
|
+
this.isExplicitlyClosed = true;
|
|
289
|
+
this.isReconnecting = false;
|
|
290
|
+
if (this.reconnectTimeout) {
|
|
291
|
+
clearTimeout(this.reconnectTimeout);
|
|
292
|
+
this.reconnectTimeout = void 0;
|
|
293
|
+
}
|
|
294
|
+
if (this.socket) {
|
|
295
|
+
this.socket.onclose = null;
|
|
296
|
+
this.socket.onerror = null;
|
|
297
|
+
this.socket.onmessage = null;
|
|
298
|
+
this.socket.onopen = null;
|
|
299
|
+
if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
|
|
300
|
+
this.socket.close();
|
|
301
|
+
}
|
|
302
|
+
this.socket = void 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Get current connection state
|
|
307
|
+
*/
|
|
308
|
+
get readyState() {
|
|
309
|
+
return this.socket?.readyState;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Check if connection is open
|
|
313
|
+
*/
|
|
314
|
+
get isOpen() {
|
|
315
|
+
return this.socket?.readyState === WebSocket.OPEN;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// src/constants.ts
|
|
320
|
+
var BASEURL = "http://192.168.1.9:8080";
|
|
321
|
+
|
|
322
|
+
// src/engine/engine.ts
|
|
323
|
+
var PelicanAuthentication = class {
|
|
324
|
+
constructor(config) {
|
|
325
|
+
this.crypto = new crypto_default();
|
|
326
|
+
this.stateMachine = new StateMachine();
|
|
327
|
+
this.sessionId = "";
|
|
328
|
+
this.sessionKey = null;
|
|
329
|
+
this.useWebSocket = true;
|
|
330
|
+
this.listeners = {};
|
|
331
|
+
if (!config.publicKey) throw new Error("Missing publicKey");
|
|
332
|
+
if (!config.projectId) throw new Error("Missing projectId");
|
|
333
|
+
if (!config.authType) throw new Error("Missing authType");
|
|
334
|
+
this.config = {
|
|
335
|
+
continuousMode: false,
|
|
336
|
+
forceQRCode: false,
|
|
337
|
+
...config
|
|
338
|
+
};
|
|
339
|
+
this.stateMachine.subscribe((s) => this.emit("state", s));
|
|
340
|
+
this.attachVisibilityRecovery();
|
|
341
|
+
}
|
|
342
|
+
/* -------------------- Public API -------------------- */
|
|
343
|
+
on(event, cb) {
|
|
344
|
+
var _a;
|
|
345
|
+
(_a = this.listeners)[event] ?? (_a[event] = /* @__PURE__ */ new Set());
|
|
346
|
+
this.listeners[event].add(cb);
|
|
347
|
+
return () => this.listeners[event].delete(cb);
|
|
348
|
+
}
|
|
349
|
+
async start() {
|
|
350
|
+
if (this.stateMachine.current !== "idle") return;
|
|
351
|
+
this.resetSession();
|
|
352
|
+
clearAuthSession();
|
|
353
|
+
this.stateMachine.transition("initializing");
|
|
354
|
+
try {
|
|
355
|
+
this.sessionKey = this.crypto.generateSymmetricKey();
|
|
356
|
+
this.sessionId = crypto.randomUUID() + crypto.randomUUID();
|
|
357
|
+
this.useWebSocket = this.shouldUseWebSocket();
|
|
358
|
+
if (this.useWebSocket) {
|
|
359
|
+
await this.startWebSocketFlow();
|
|
360
|
+
} else {
|
|
361
|
+
await this.startDeepLinkFlow();
|
|
362
|
+
}
|
|
363
|
+
} catch (err) {
|
|
364
|
+
this.fail(err instanceof Error ? err : new Error("Start failed"));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
stop() {
|
|
368
|
+
this.terminate(false);
|
|
369
|
+
}
|
|
370
|
+
destroy() {
|
|
371
|
+
this.detachVisibilityRecovery();
|
|
372
|
+
this.clearBackupCheck();
|
|
373
|
+
this.transport?.close();
|
|
374
|
+
this.transport = void 0;
|
|
375
|
+
this.listeners = {};
|
|
376
|
+
}
|
|
377
|
+
/* -------------------- Flow Selection -------------------- */
|
|
378
|
+
shouldUseWebSocket() {
|
|
379
|
+
return this.config.forceQRCode || !/Android|iPhone|iPad/i.test(navigator.userAgent);
|
|
380
|
+
}
|
|
381
|
+
/* -------------------- WebSocket Flow (QR Code) -------------------- */
|
|
382
|
+
async startWebSocketFlow() {
|
|
383
|
+
const relay = await this.fetchRelayUrl();
|
|
384
|
+
this.transport = new Transport({
|
|
385
|
+
onOpen: () => {
|
|
386
|
+
this.transport.send({
|
|
387
|
+
type: "register",
|
|
388
|
+
sessionID: this.sessionId,
|
|
389
|
+
...this.config
|
|
390
|
+
});
|
|
391
|
+
this.stateMachine.transition("awaiting-pair");
|
|
392
|
+
},
|
|
393
|
+
onMessage: (msg) => this.handleWebSocketMessage(msg),
|
|
394
|
+
onError: (err) => {
|
|
395
|
+
console.error("WebSocket error:", err);
|
|
396
|
+
this.fail(new Error("WebSocket connection failed"));
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
this.transport.connect(relay);
|
|
400
|
+
await this.emitQRCode();
|
|
401
|
+
}
|
|
402
|
+
handleWebSocketMessage(msg) {
|
|
403
|
+
switch (msg.type) {
|
|
404
|
+
case "paired":
|
|
405
|
+
this.stateMachine.transition("paired");
|
|
406
|
+
this.transport.send({
|
|
407
|
+
type: "authenticate",
|
|
408
|
+
sessionID: this.sessionId,
|
|
409
|
+
...this.config,
|
|
410
|
+
url: window.location.href
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
case "phone-auth-success":
|
|
414
|
+
this.handleAuthSuccess(msg);
|
|
415
|
+
return;
|
|
416
|
+
case "phone-terminated":
|
|
417
|
+
this.fail(new Error("Authentication cancelled on device"));
|
|
418
|
+
this.restartIfContinuous();
|
|
419
|
+
return;
|
|
420
|
+
case "confirmed":
|
|
421
|
+
this.terminate(true);
|
|
422
|
+
this.restartIfContinuous();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
handleAuthSuccess(msg) {
|
|
427
|
+
if (!this.sessionKey || !msg.cipher || !msg.nonce) {
|
|
428
|
+
this.fail(new Error("Invalid authentication payload"));
|
|
429
|
+
this.restartIfContinuous();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
const decrypted = this.crypto.decryptSymmetric({
|
|
434
|
+
encrypted: { cipher: msg.cipher, nonce: msg.nonce },
|
|
435
|
+
keyString: this.sessionKey
|
|
436
|
+
});
|
|
437
|
+
if (!decrypted) {
|
|
438
|
+
this.fail(new Error("Invalid authentication data"));
|
|
439
|
+
this.restartIfContinuous();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const result = JSON.parse(decrypted);
|
|
443
|
+
this.emit("success", result);
|
|
444
|
+
this.stateMachine.transition("authenticated");
|
|
445
|
+
this.transport?.send({
|
|
446
|
+
type: "confirm",
|
|
447
|
+
sessionID: this.sessionId
|
|
448
|
+
});
|
|
449
|
+
} catch {
|
|
450
|
+
this.fail(new Error("Failed to decrypt authentication data"));
|
|
451
|
+
this.restartIfContinuous();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/* -------------------- Deep Link Flow (Mobile) -------------------- */
|
|
455
|
+
async startDeepLinkFlow() {
|
|
456
|
+
await this.fetchRelayUrl();
|
|
457
|
+
if (this.sessionKey) {
|
|
458
|
+
storeAuthSession(this.sessionId, this.sessionKey, 10 * 6e4);
|
|
459
|
+
}
|
|
460
|
+
await this.emitDeepLink();
|
|
461
|
+
this.stateMachine.transition("awaiting-pair");
|
|
462
|
+
}
|
|
463
|
+
clearBackupCheck() {
|
|
464
|
+
if (this.backupCheckTimeout) {
|
|
465
|
+
clearTimeout(this.backupCheckTimeout);
|
|
466
|
+
this.backupCheckTimeout = void 0;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async checkSession() {
|
|
470
|
+
const cached = getAuthSession();
|
|
471
|
+
if (!cached) return;
|
|
472
|
+
this.stateMachine.transition("awaiting-auth");
|
|
473
|
+
try {
|
|
474
|
+
const res = await fetch(
|
|
475
|
+
`${BASEURL}/session?session_id=${cached.sessionId}`
|
|
476
|
+
);
|
|
477
|
+
if (!res.ok) return;
|
|
478
|
+
const data = await res.json();
|
|
479
|
+
const decrypted = this.crypto.decryptSymmetric({
|
|
480
|
+
encrypted: { cipher: data.cipher, nonce: data.nonce },
|
|
481
|
+
keyString: cached.sessionKey
|
|
482
|
+
});
|
|
483
|
+
if (!decrypted) {
|
|
484
|
+
console.warn("Failed to decrypt session");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const result = JSON.parse(decrypted);
|
|
488
|
+
this.clearBackupCheck();
|
|
489
|
+
clearAuthSession();
|
|
490
|
+
this.emit("success", result);
|
|
491
|
+
this.stateMachine.transition("authenticated");
|
|
492
|
+
if (this.config.continuousMode) {
|
|
493
|
+
setTimeout(() => {
|
|
494
|
+
this.stateMachine.transition("confirmed");
|
|
495
|
+
this.start();
|
|
496
|
+
}, 1500);
|
|
497
|
+
} else {
|
|
498
|
+
this.stateMachine.transition("confirmed");
|
|
499
|
+
}
|
|
500
|
+
} catch (err) {
|
|
501
|
+
console.debug("Session check failed:", err);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/* -------------------- Entry Point Generation -------------------- */
|
|
505
|
+
async emitQRCode() {
|
|
506
|
+
const payload = {
|
|
507
|
+
sessionID: this.sessionId,
|
|
508
|
+
sessionKey: this.sessionKey,
|
|
509
|
+
publicKey: this.config.publicKey,
|
|
510
|
+
authType: this.config.authType,
|
|
511
|
+
projectId: this.config.projectId,
|
|
512
|
+
url: window.location.href
|
|
513
|
+
};
|
|
514
|
+
const qr = await QRCode__default.default.toDataURL(JSON.stringify(payload), {
|
|
515
|
+
type: "image/png",
|
|
516
|
+
scale: 3,
|
|
517
|
+
color: { light: "#ffffff", dark: "#424242ff" }
|
|
518
|
+
});
|
|
519
|
+
this.emit("qr", qr);
|
|
520
|
+
}
|
|
521
|
+
async emitDeepLink() {
|
|
522
|
+
const deeplink = `pelicanvault://auth/deep-link?sessionID=${encodeURIComponent(
|
|
523
|
+
this.sessionId
|
|
524
|
+
)}&sessionKey=${encodeURIComponent(
|
|
525
|
+
this.sessionKey
|
|
526
|
+
)}&publicKey=${encodeURIComponent(this.config.publicKey)}&authType=${this.config.authType}&projectId=${encodeURIComponent(
|
|
527
|
+
this.config.projectId
|
|
528
|
+
)}&url=${encodeURIComponent(window.location.href)}`;
|
|
529
|
+
this.emit("deeplink", deeplink);
|
|
530
|
+
}
|
|
531
|
+
/* -------------------- Visibility Recovery -------------------- */
|
|
532
|
+
attachVisibilityRecovery() {
|
|
533
|
+
this.visibilityHandler = async () => {
|
|
534
|
+
if (document.visibilityState !== "visible") return;
|
|
535
|
+
if (this.useWebSocket) return;
|
|
536
|
+
await this.checkSession();
|
|
537
|
+
};
|
|
538
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
539
|
+
}
|
|
540
|
+
detachVisibilityRecovery() {
|
|
541
|
+
if (this.visibilityHandler) {
|
|
542
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
543
|
+
this.visibilityHandler = void 0;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/* -------------------- Helpers -------------------- */
|
|
547
|
+
async fetchRelayUrl() {
|
|
548
|
+
const { publicKey, projectId, authType } = this.config;
|
|
549
|
+
const res = await fetch(
|
|
550
|
+
`${BASEURL}/relay?public_key=${publicKey}&auth_type=${authType}&project_id=${projectId}`
|
|
551
|
+
);
|
|
552
|
+
if (!res.ok) {
|
|
553
|
+
const error = await res.text();
|
|
554
|
+
throw new Error(error);
|
|
555
|
+
}
|
|
556
|
+
const json = await res.json();
|
|
557
|
+
return json.relay_url;
|
|
558
|
+
}
|
|
559
|
+
terminate(success) {
|
|
560
|
+
this.clearBackupCheck();
|
|
561
|
+
if (this.transport) {
|
|
562
|
+
if (!success) {
|
|
563
|
+
this.transport.send({
|
|
564
|
+
type: "client-terminated",
|
|
565
|
+
sessionID: this.sessionId,
|
|
566
|
+
projectId: this.config.projectId,
|
|
567
|
+
publicKey: this.config.publicKey,
|
|
568
|
+
authType: this.config.authType
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
this.transport.close();
|
|
572
|
+
this.transport = void 0;
|
|
573
|
+
}
|
|
574
|
+
clearAuthSession();
|
|
575
|
+
this.resetSession();
|
|
576
|
+
if (!this.config.continuousMode) {
|
|
577
|
+
this.detachVisibilityRecovery();
|
|
578
|
+
}
|
|
579
|
+
this.stateMachine.transition(success ? "confirmed" : "idle");
|
|
580
|
+
}
|
|
581
|
+
restartIfContinuous() {
|
|
582
|
+
if (!this.config.continuousMode) return;
|
|
583
|
+
this.clearBackupCheck();
|
|
584
|
+
this.transport?.close();
|
|
585
|
+
this.transport = void 0;
|
|
586
|
+
this.stateMachine.transition("idle");
|
|
587
|
+
setTimeout(() => {
|
|
588
|
+
this.start();
|
|
589
|
+
}, 150);
|
|
590
|
+
}
|
|
591
|
+
resetSession() {
|
|
592
|
+
this.sessionId = "";
|
|
593
|
+
this.sessionKey = null;
|
|
594
|
+
}
|
|
595
|
+
emit(event, payload) {
|
|
596
|
+
this.listeners[event]?.forEach((cb) => cb(payload));
|
|
597
|
+
}
|
|
598
|
+
fail(err) {
|
|
599
|
+
this.emit("error", err);
|
|
600
|
+
this.stateMachine.transition("error");
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
exports.BASEURL = BASEURL;
|
|
605
|
+
exports.CryptoService = CryptoService;
|
|
606
|
+
exports.PelicanAuthentication = PelicanAuthentication;
|
|
607
|
+
exports.StateMachine = StateMachine;
|
|
608
|
+
exports.Transport = Transport;
|
|
609
|
+
exports.clearAllAuthData = clearAllAuthData;
|
|
610
|
+
exports.clearAuthSession = clearAuthSession;
|
|
611
|
+
exports.getAuthSession = getAuthSession;
|
|
612
|
+
exports.storeAuthSession = storeAuthSession;
|
|
613
|
+
//# sourceMappingURL=index.js.map
|
|
8
614
|
//# sourceMappingURL=index.js.map
|