@arcblock/did-connect-core 4.0.1 → 4.0.2

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.
Files changed (2) hide show
  1. package/dist/index.cjs +1378 -0
  2. package/package.json +4 -3
package/dist/index.cjs ADDED
@@ -0,0 +1,1378 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ConnectError: () => ConnectError,
24
+ DEFAULTS: () => DEFAULTS,
25
+ DID_PREFIX: () => DID_PREFIX,
26
+ ERROR_CODES: () => ERROR_CODES,
27
+ EmailFlow: () => EmailFlow,
28
+ FetchHttpAdapter: () => FetchHttpAdapter,
29
+ OAuthFlow: () => OAuthFlow,
30
+ PasskeyFlow: () => PasskeyFlow,
31
+ STORAGE_KEYS: () => STORAGE_KEYS,
32
+ SessionManager: () => SessionManager,
33
+ TERMINAL_STATUSES: () => TERMINAL_STATUSES,
34
+ TOKEN_STATUS: () => TOKEN_STATUS,
35
+ TokenSession: () => TokenSession,
36
+ VERSION: () => VERSION,
37
+ buildActionPrefix: () => buildActionPrefix,
38
+ decodeConnectUrl: () => decodeConnectUrl,
39
+ encodeConnectUrl: () => encodeConnectUrl,
40
+ getApiErrorMessage: () => getApiErrorMessage,
41
+ getBrowserLocale: () => getBrowserLocale,
42
+ getConnectedInfo: () => getConnectedInfo,
43
+ getDidChainLabel: () => getDidChainLabel,
44
+ getDidDisplayParts: () => getDidDisplayParts,
45
+ getWebAuthnErrorMessage: () => getWebAuthnErrorMessage,
46
+ isEthereumDid: () => isEthereumDid,
47
+ openPopup: () => openPopup,
48
+ parseNextWorkflow: () => parseNextWorkflow,
49
+ parseTokenFromUrl: () => parseTokenFromUrl,
50
+ stringifyParams: () => stringifyParams,
51
+ truncateDid: () => truncateDid,
52
+ waitForPopupMessage: () => waitForPopupMessage
53
+ });
54
+ module.exports = __toCommonJS(index_exports);
55
+
56
+ // src/constants.ts
57
+ var TOKEN_STATUS = {
58
+ CREATED: "created",
59
+ SCANNED: "scanned",
60
+ SUCCEED: "succeed",
61
+ ERROR: "error",
62
+ TIMEOUT: "timeout",
63
+ BUSY: "busy",
64
+ FORBIDDEN: "forbidden"
65
+ };
66
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["succeed", "error", "timeout"]);
67
+ var DEFAULTS = {
68
+ API_PREFIX: "/api/did",
69
+ SERVICE_PREFIX: "/.well-known/service",
70
+ ACTION: "login",
71
+ TOKEN_TIMEOUT: 3e5,
72
+ CHECK_INTERVAL: 2e3,
73
+ TOKEN_KEY: "_t_",
74
+ TOKEN_DELIVERY: "cookie",
75
+ CREATE_THROTTLE: 500,
76
+ AUTO_CHECK_INTERVAL: 6e4
77
+ };
78
+ var STORAGE_KEYS = {
79
+ SESSION_TOKEN: "login_token",
80
+ REFRESH_TOKEN: "refresh_token",
81
+ ENC_KEY: "__encKey",
82
+ DEC_KEY: "__decKey",
83
+ CSRF_TOKEN: "x-csrf-token",
84
+ CONNECTED_DID: "connected_did",
85
+ CONNECTED_PK: "connected_pk",
86
+ CONNECTED_APP: "connected_app",
87
+ CONNECTED_WALLET_OS: "connected_wallet_os"
88
+ };
89
+
90
+ // src/did-display.ts
91
+ var DID_PREFIX = "did:abt:";
92
+ function isEthereumDid(did) {
93
+ if (!did || typeof did !== "string") return false;
94
+ const address = did.replace(DID_PREFIX, "");
95
+ return /^(0x)?[0-9a-f]{40}$/i.test(address);
96
+ }
97
+ function truncateDid(did, startChars = 8, endChars = 6) {
98
+ if (!did || typeof did !== "string") return "";
99
+ if (did.length <= startChars + endChars + 3) return did;
100
+ return `${did.slice(0, startChars)}...${did.slice(-endChars)}`;
101
+ }
102
+ function getDidChainLabel(did) {
103
+ return isEthereumDid(did) ? "ETH" : "ABT";
104
+ }
105
+ function getDidDisplayParts(did, options = {}) {
106
+ if (!did || typeof did !== "string") {
107
+ return { prefix: "DID: ABT:", chainLabel: "ABT", address: "", compact: "", copyText: "" };
108
+ }
109
+ const { startChars = 8, endChars = 6 } = options;
110
+ const isEth = isEthereumDid(did);
111
+ const address = did.replace(DID_PREFIX, "");
112
+ const chainLabel = isEth ? "ETH" : "ABT";
113
+ const includePrefix = options.includePrefix ?? !isEth;
114
+ return {
115
+ prefix: `DID: ${chainLabel}:`,
116
+ chainLabel,
117
+ address,
118
+ compact: truncateDid(address, startChars, endChars),
119
+ copyText: includePrefix ? `${DID_PREFIX}${address}` : address
120
+ };
121
+ }
122
+
123
+ // src/errors.ts
124
+ var ERROR_CODES = {
125
+ TOKEN_EXPIRED: "TOKEN_EXPIRED",
126
+ TOKEN_NOT_FOUND: "TOKEN_NOT_FOUND",
127
+ TOKEN_TIMEOUT: "TOKEN_TIMEOUT",
128
+ ALREADY_EXIST: "ALREADY_EXIST",
129
+ ALREADY_BIND: "ALREADY_BIND",
130
+ NETWORK_ERROR: "NETWORK_ERROR",
131
+ INVALID_RESPONSE: "INVALID_RESPONSE",
132
+ UNAUTHORIZED: "UNAUTHORIZED",
133
+ FORBIDDEN: "FORBIDDEN"
134
+ };
135
+ var ConnectError = class extends Error {
136
+ code;
137
+ constructor(code, message) {
138
+ super(message);
139
+ this.name = "ConnectError";
140
+ this.code = code;
141
+ }
142
+ };
143
+
144
+ // src/email-flow.ts
145
+ var EmailFlow = class {
146
+ _servicePrefix;
147
+ _http;
148
+ constructor(config, http) {
149
+ this._servicePrefix = config.servicePrefix ?? DEFAULTS.SERVICE_PREFIX;
150
+ this._http = http;
151
+ }
152
+ /**
153
+ * Send a verification code to the given email address.
154
+ * POST {servicePrefix}/api/email/sendCode
155
+ * Returns { id: string } — the code ID for subsequent checkStatus.
156
+ */
157
+ async sendCode(email, locale) {
158
+ if (email == null) {
159
+ throw new ConnectError(ERROR_CODES.INVALID_RESPONSE, "Email is required");
160
+ }
161
+ const url = `${this._servicePrefix}/api/email/sendCode`;
162
+ return this._http.post(url, { email, locale });
163
+ }
164
+ /**
165
+ * Check verification code status.
166
+ * GET {servicePrefix}/api/email/status?codeId=...
167
+ */
168
+ async checkStatus(codeId) {
169
+ if (!codeId) {
170
+ throw new ConnectError(ERROR_CODES.INVALID_RESPONSE, "codeId is required");
171
+ }
172
+ const url = `${this._servicePrefix}/api/email/status`;
173
+ return this._http.get(url, {
174
+ params: { codeId }
175
+ });
176
+ }
177
+ /**
178
+ * Login with verification code or magic token.
179
+ * POST {servicePrefix}/api/email/login
180
+ * Returns AuthResult (same shape as OAuth login).
181
+ */
182
+ async login(params) {
183
+ const url = `${this._servicePrefix}/api/email/login`;
184
+ return this._http.post(url, params);
185
+ }
186
+ };
187
+
188
+ // src/fetch-adapter.ts
189
+ var FetchHttpAdapter = class {
190
+ _defaultHeaders;
191
+ _onRequest;
192
+ constructor(options) {
193
+ this._defaultHeaders = options?.headers ?? {};
194
+ this._onRequest = options?.onRequest;
195
+ }
196
+ async get(url, options) {
197
+ this._validateUrl(url);
198
+ const queryString = options?.params ? this._serializeParams(options.params) : "";
199
+ const fullUrl = queryString ? `${url}${url.includes("?") ? "&" : "?"}${queryString}` : url;
200
+ const headers = {
201
+ ...this._defaultHeaders,
202
+ ...options?.headers
203
+ };
204
+ let init = {
205
+ method: "GET",
206
+ headers,
207
+ signal: options?.signal
208
+ };
209
+ if (this._onRequest) {
210
+ init = this._onRequest(fullUrl, init);
211
+ }
212
+ return this._execute(fullUrl, init);
213
+ }
214
+ async post(url, body, options) {
215
+ this._validateUrl(url);
216
+ const queryString = options?.params ? this._serializeParams(options.params) : "";
217
+ const fullUrl = queryString ? `${url}${url.includes("?") ? "&" : "?"}${queryString}` : url;
218
+ const headers = {
219
+ "Content-Type": "application/json",
220
+ ...this._defaultHeaders,
221
+ ...options?.headers
222
+ };
223
+ let init = {
224
+ method: "POST",
225
+ headers,
226
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
227
+ signal: options?.signal
228
+ };
229
+ if (this._onRequest) {
230
+ init = this._onRequest(fullUrl, init);
231
+ }
232
+ return this._execute(fullUrl, init);
233
+ }
234
+ _validateUrl(url) {
235
+ if (!url?.trim()) {
236
+ throw new ConnectError(ERROR_CODES.NETWORK_ERROR, "Request URL must not be empty");
237
+ }
238
+ if (url.trim().toLowerCase().startsWith("javascript:")) {
239
+ throw new ConnectError(ERROR_CODES.NETWORK_ERROR, "Invalid URL protocol");
240
+ }
241
+ }
242
+ async _execute(url, init) {
243
+ let response;
244
+ try {
245
+ response = await globalThis.fetch(url, init);
246
+ } catch (err) {
247
+ if (err?.name === "AbortError") {
248
+ throw err;
249
+ }
250
+ throw new ConnectError(ERROR_CODES.NETWORK_ERROR, `Network request failed: ${err?.message ?? "Unknown error"}`);
251
+ }
252
+ if (!response.ok) {
253
+ let errorMessage = "";
254
+ let errorCode = "";
255
+ try {
256
+ const body = await response.text();
257
+ try {
258
+ const json = JSON.parse(body);
259
+ if (json.error) errorMessage = json.error;
260
+ if (json.code) errorCode = json.code;
261
+ } catch {
262
+ errorMessage = body;
263
+ }
264
+ } catch {
265
+ }
266
+ const code = errorCode === "FORBIDDEN" ? ERROR_CODES.FORBIDDEN : ERROR_CODES.NETWORK_ERROR;
267
+ throw new ConnectError(
268
+ code,
269
+ errorMessage || `HTTP ${response.status} ${response.statusText || "Error"}`
270
+ );
271
+ }
272
+ if (response.status === 204) {
273
+ return null;
274
+ }
275
+ const text = await response.text();
276
+ if (!text.trim()) {
277
+ return null;
278
+ }
279
+ const contentType = response.headers.get("content-type") ?? "";
280
+ if (!contentType.includes("application/json") && !contentType.includes("+json")) {
281
+ throw new ConnectError(ERROR_CODES.INVALID_RESPONSE, `Expected JSON response but received: ${contentType || "unknown content type"}`);
282
+ }
283
+ try {
284
+ return JSON.parse(text);
285
+ } catch {
286
+ throw new ConnectError(ERROR_CODES.INVALID_RESPONSE, "Failed to parse JSON response");
287
+ }
288
+ }
289
+ _serializeParams(params) {
290
+ const parts = [];
291
+ for (const key of Object.keys(params)) {
292
+ const value = params[key];
293
+ if (value === null || value === void 0) {
294
+ continue;
295
+ }
296
+ if (Array.isArray(value)) {
297
+ for (const item of value) {
298
+ if (item === null || item === void 0) {
299
+ continue;
300
+ }
301
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`);
302
+ }
303
+ } else if (typeof value === "boolean") {
304
+ parts.push(`${encodeURIComponent(key)}=${value ? "true" : "false"}`);
305
+ } else {
306
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
307
+ }
308
+ }
309
+ return parts.join("&");
310
+ }
311
+ };
312
+
313
+ // src/oauth-flow.ts
314
+ var OAuthFlow = class {
315
+ _servicePrefix;
316
+ _http;
317
+ constructor(config, http) {
318
+ this._servicePrefix = config.servicePrefix ?? DEFAULTS.SERVICE_PREFIX;
319
+ this._http = http;
320
+ }
321
+ /**
322
+ * Fetch enabled OAuth providers.
323
+ * GET {servicePrefix}/api/oauth/configs
324
+ * Response: { providers: OAuthProvider[] }
325
+ */
326
+ async getProviders() {
327
+ const url = `${this._servicePrefix}/api/oauth/configs`;
328
+ const res = await this._http.get(url);
329
+ if (!res || !Array.isArray(res.providers)) {
330
+ return [];
331
+ }
332
+ return res.providers;
333
+ }
334
+ /**
335
+ * Exchange an OAuth authorization code for a session.
336
+ * POST {servicePrefix}/api/oauth/login
337
+ */
338
+ async exchangeCode(provider, code, params) {
339
+ if (code == null) {
340
+ throw new ConnectError(ERROR_CODES.INVALID_RESPONSE, "OAuth code is required");
341
+ }
342
+ const url = `${this._servicePrefix}/api/oauth/login`;
343
+ return this._http.post(url, { provider, code, ...params });
344
+ }
345
+ /**
346
+ * Bind an OAuth account to the current user.
347
+ * POST {servicePrefix}/api/oauth/bind
348
+ */
349
+ async bind(provider, code, params) {
350
+ const url = `${this._servicePrefix}/api/oauth/bind`;
351
+ return this._http.post(url, { provider, code, ...params });
352
+ }
353
+ /**
354
+ * Unbind an OAuth account.
355
+ * POST {servicePrefix}/api/oauth/unbind
356
+ */
357
+ async unbind(provider, accountDid) {
358
+ const url = `${this._servicePrefix}/api/oauth/unbind`;
359
+ return this._http.post(url, { provider, accountDid });
360
+ }
361
+ /**
362
+ * Fetch available OAuth passports (switchable accounts).
363
+ * GET {servicePrefix}/api/oauth/passports
364
+ */
365
+ async getPassports() {
366
+ const url = `${this._servicePrefix}/api/oauth/passports`;
367
+ const res = await this._http.get(url);
368
+ return Array.isArray(res) ? res : [];
369
+ }
370
+ };
371
+
372
+ // src/passkey-flow.ts
373
+ var PasskeyFlow = class {
374
+ _servicePrefix;
375
+ _http;
376
+ constructor(config, http) {
377
+ this._servicePrefix = config.servicePrefix ?? DEFAULTS.SERVICE_PREFIX;
378
+ this._http = http;
379
+ }
380
+ /**
381
+ * Get WebAuthn registration options (challenge, etc.).
382
+ * GET {servicePrefix}/api/passkey/register
383
+ */
384
+ async getRegisterOptions(params) {
385
+ const url = `${this._servicePrefix}/api/passkey/register`;
386
+ return this._http.get(url, { params });
387
+ }
388
+ /**
389
+ * Submit a registration credential.
390
+ * POST {servicePrefix}/api/passkey/register?challenge=...
391
+ */
392
+ async submitRegistration(credential, params) {
393
+ if (credential == null) {
394
+ throw new ConnectError(ERROR_CODES.INVALID_RESPONSE, "Credential is required");
395
+ }
396
+ const url = `${this._servicePrefix}/api/passkey/register`;
397
+ return this._http.post(url, credential, { params });
398
+ }
399
+ /**
400
+ * Get WebAuthn authentication options (challenge, etc.).
401
+ * GET {servicePrefix}/api/passkey/auth
402
+ */
403
+ async getAuthOptions(params) {
404
+ const url = `${this._servicePrefix}/api/passkey/auth`;
405
+ return this._http.get(url, { params });
406
+ }
407
+ /**
408
+ * Submit an authentication credential.
409
+ * POST {servicePrefix}/api/passkey/auth?challenge=...&targetAppPid=...
410
+ */
411
+ async submitAuthentication(credential, params) {
412
+ if (credential == null) {
413
+ throw new ConnectError(ERROR_CODES.INVALID_RESPONSE, "Credential is required");
414
+ }
415
+ const url = `${this._servicePrefix}/api/passkey/auth`;
416
+ return this._http.post(url, credential, { params });
417
+ }
418
+ /**
419
+ * Fetch passkey passport list.
420
+ * GET {servicePrefix}/api/passkey/passports
421
+ */
422
+ async getPassports() {
423
+ const url = `${this._servicePrefix}/api/passkey/passports`;
424
+ const res = await this._http.get(url);
425
+ return Array.isArray(res) ? res : [];
426
+ }
427
+ };
428
+
429
+ // src/session-manager.ts
430
+ var SessionManager = class {
431
+ // -- state ----------------------------------------------------------------
432
+ _state = {
433
+ user: null,
434
+ loading: false,
435
+ error: null,
436
+ initialized: false
437
+ };
438
+ // -- deps -----------------------------------------------------------------
439
+ _config;
440
+ _http;
441
+ _storage;
442
+ // -- token keys -----------------------------------------------------------
443
+ _tokenKey;
444
+ _refreshTokenKey;
445
+ // -- auto-check -----------------------------------------------------------
446
+ _autoCheckTimer = null;
447
+ _autoCheckInterval;
448
+ // -- events ---------------------------------------------------------------
449
+ _listeners = /* @__PURE__ */ new Map();
450
+ // -- concurrency ----------------------------------------------------------
451
+ _refreshing = null;
452
+ // -- lifecycle ------------------------------------------------------------
453
+ _destroyed = false;
454
+ // =========================================================================
455
+ // Constructor
456
+ // =========================================================================
457
+ constructor(config, http, storage, options) {
458
+ this._config = {
459
+ ...config,
460
+ apiPrefix: config.apiPrefix ?? DEFAULTS.API_PREFIX,
461
+ action: config.action ?? DEFAULTS.ACTION,
462
+ servicePrefix: config.servicePrefix ?? DEFAULTS.SERVICE_PREFIX,
463
+ tokenTimeout: config.tokenTimeout ?? DEFAULTS.TOKEN_TIMEOUT,
464
+ checkInterval: config.checkInterval ?? DEFAULTS.CHECK_INTERVAL,
465
+ tokenDelivery: config.tokenDelivery ?? DEFAULTS.TOKEN_DELIVERY
466
+ };
467
+ this._http = http;
468
+ this._storage = storage;
469
+ this._tokenKey = options?.tokenKey ?? STORAGE_KEYS.SESSION_TOKEN;
470
+ this._refreshTokenKey = options?.refreshTokenKey ?? STORAGE_KEYS.REFRESH_TOKEN;
471
+ this._autoCheckInterval = options?.autoCheckInterval ?? DEFAULTS.AUTO_CHECK_INTERVAL;
472
+ }
473
+ // =========================================================================
474
+ // Public getters
475
+ // =========================================================================
476
+ get state() {
477
+ return this._state;
478
+ }
479
+ // =========================================================================
480
+ // Public methods
481
+ // =========================================================================
482
+ /**
483
+ * Fetch the current session from the server.
484
+ *
485
+ * The server may rotate tokens transparently — if `nextToken` /
486
+ * `nextRefreshToken` are present in the response they are persisted and
487
+ * a `tokenRefreshed` event is emitted.
488
+ */
489
+ async getSession() {
490
+ const url = `${this._config.apiPrefix}/session`;
491
+ const token = this._storage.get(this._tokenKey);
492
+ const headers = {};
493
+ if (token) {
494
+ headers.Authorization = `Bearer ${token}`;
495
+ }
496
+ try {
497
+ const res = await this._http.get(url, { headers });
498
+ if (res.nextToken) {
499
+ this._storage.set(this._tokenKey, res.nextToken);
500
+ }
501
+ if (res.nextRefreshToken) {
502
+ this._storage.set(this._refreshTokenKey, res.nextRefreshToken);
503
+ }
504
+ if (res.nextToken || res.nextRefreshToken) {
505
+ this._emit("tokenRefreshed");
506
+ }
507
+ const user = res.user ?? null;
508
+ this._setState({ user, error: null });
509
+ this._emit("userChange", user);
510
+ return user;
511
+ } catch (err) {
512
+ if (_isUnauthorized(err)) {
513
+ try {
514
+ await this.refreshSession();
515
+ return await this._getSessionDirect();
516
+ } catch {
517
+ }
518
+ }
519
+ const message = _errorMessage(err);
520
+ this._setState({ error: message });
521
+ this._emit("error", new ConnectError(ERROR_CODES.NETWORK_ERROR, message));
522
+ return null;
523
+ }
524
+ }
525
+ /**
526
+ * Refresh the session token using the stored refresh token.
527
+ *
528
+ * Concurrent calls are de-duplicated: only the first caller performs the
529
+ * network request; subsequent callers await the same promise.
530
+ */
531
+ async refreshSession() {
532
+ if (this._refreshing !== null) {
533
+ await this._refreshing;
534
+ return;
535
+ }
536
+ let resolve;
537
+ let _reject;
538
+ this._refreshing = new Promise((res, rej) => {
539
+ resolve = res;
540
+ _reject = rej;
541
+ });
542
+ try {
543
+ const refreshToken = this._storage.get(this._refreshTokenKey);
544
+ if (!refreshToken) {
545
+ await this.logout();
546
+ resolve();
547
+ return;
548
+ }
549
+ const url = `${this._config.apiPrefix}/refreshSession`;
550
+ const res = await this._http.post(url, null, {
551
+ headers: { Authorization: `Bearer ${refreshToken}` }
552
+ });
553
+ const newToken = res.token ?? res.sessionToken;
554
+ const newRefreshToken = res.refreshToken;
555
+ if (newToken) {
556
+ this._storage.set(this._tokenKey, newToken);
557
+ }
558
+ if (newRefreshToken) {
559
+ this._storage.set(this._refreshTokenKey, newRefreshToken);
560
+ }
561
+ this._emit("tokenRefreshed");
562
+ resolve();
563
+ } catch (_err) {
564
+ await this.logout();
565
+ resolve();
566
+ } finally {
567
+ this._refreshing = null;
568
+ }
569
+ }
570
+ /**
571
+ * Clear tokens and reset session state.
572
+ */
573
+ async logout() {
574
+ try {
575
+ this._storage.remove(this._tokenKey);
576
+ } catch {
577
+ }
578
+ try {
579
+ this._storage.remove(this._refreshTokenKey);
580
+ } catch {
581
+ }
582
+ this._setState({ user: null, error: null });
583
+ this._emit("logout");
584
+ this._emit("userChange", null);
585
+ }
586
+ /**
587
+ * Whether a user is currently authenticated.
588
+ */
589
+ isAuthenticated() {
590
+ return this._state.user !== null;
591
+ }
592
+ /**
593
+ * Restore session from storage on startup.
594
+ *
595
+ * Typically called once during application bootstrap. Sets `initialized`
596
+ * to `true` when complete regardless of outcome.
597
+ */
598
+ async restore() {
599
+ this._setState({ loading: true });
600
+ try {
601
+ const token = this._storage.get(this._tokenKey);
602
+ if (token) {
603
+ await this.getSession();
604
+ } else {
605
+ this._setState({ user: null });
606
+ }
607
+ } catch {
608
+ } finally {
609
+ this._setState({ initialized: true, loading: false });
610
+ this._emit("userChange", this._state.user);
611
+ }
612
+ }
613
+ /**
614
+ * Start a periodic timer that checks whether the current token is past
615
+ * its midlife point and proactively refreshes it.
616
+ */
617
+ startAutoCheck() {
618
+ if (this._autoCheckTimer !== null) {
619
+ this.stopAutoCheck();
620
+ }
621
+ this._autoCheckTimer = setInterval(() => {
622
+ this._checkToken();
623
+ }, this._autoCheckInterval);
624
+ }
625
+ /**
626
+ * Stop the periodic auto-check timer.
627
+ */
628
+ stopAutoCheck() {
629
+ if (this._autoCheckTimer !== null) {
630
+ clearInterval(this._autoCheckTimer);
631
+ this._autoCheckTimer = null;
632
+ }
633
+ }
634
+ /**
635
+ * Tear down the manager. Idempotent — safe to call multiple times.
636
+ */
637
+ destroy() {
638
+ if (this._destroyed) {
639
+ return;
640
+ }
641
+ this.stopAutoCheck();
642
+ this._listeners.clear();
643
+ this._destroyed = true;
644
+ }
645
+ // =========================================================================
646
+ // Event methods
647
+ // =========================================================================
648
+ /**
649
+ * Register an event handler. Returns `this` for chaining.
650
+ */
651
+ on(event, handler) {
652
+ let set = this._listeners.get(event);
653
+ if (!set) {
654
+ set = /* @__PURE__ */ new Set();
655
+ this._listeners.set(event, set);
656
+ }
657
+ set.add(handler);
658
+ return this;
659
+ }
660
+ /**
661
+ * Remove a previously registered event handler. Returns `this` for chaining.
662
+ */
663
+ off(event, handler) {
664
+ const set = this._listeners.get(event);
665
+ if (set) {
666
+ set.delete(handler);
667
+ if (set.size === 0) {
668
+ this._listeners.delete(event);
669
+ }
670
+ }
671
+ return this;
672
+ }
673
+ // =========================================================================
674
+ // Private helpers
675
+ // =========================================================================
676
+ /**
677
+ * Emit an event to all registered listeners.
678
+ */
679
+ _emit(event, ...args) {
680
+ const set = this._listeners.get(event);
681
+ if (!set) return;
682
+ for (const handler of set) {
683
+ try {
684
+ handler(...args);
685
+ } catch {
686
+ }
687
+ }
688
+ }
689
+ /**
690
+ * Merge a partial state update into `_state`.
691
+ */
692
+ _setState(patch) {
693
+ this._state = { ...this._state, ...patch };
694
+ }
695
+ /**
696
+ * Direct HTTP call for session — used as the single retry path after
697
+ * refreshSession() to avoid infinite recursion through getSession().
698
+ */
699
+ async _getSessionDirect() {
700
+ const url = `${this._config.apiPrefix}/session`;
701
+ const token = this._storage.get(this._tokenKey);
702
+ const headers = {};
703
+ if (token) {
704
+ headers.Authorization = `Bearer ${token}`;
705
+ }
706
+ const res = await this._http.get(url, { headers });
707
+ if (res.nextToken) {
708
+ this._storage.set(this._tokenKey, res.nextToken);
709
+ }
710
+ if (res.nextRefreshToken) {
711
+ this._storage.set(this._refreshTokenKey, res.nextRefreshToken);
712
+ }
713
+ if (res.nextToken || res.nextRefreshToken) {
714
+ this._emit("tokenRefreshed");
715
+ }
716
+ const user = res.user ?? null;
717
+ this._setState({ user, error: null });
718
+ this._emit("userChange", user);
719
+ return user;
720
+ }
721
+ /**
722
+ * Check whether the current token is past its midlife and, if so,
723
+ * proactively trigger a refresh.
724
+ *
725
+ * "Midlife" means more than half of the token's lifetime has elapsed:
726
+ * `(exp - now) < (now - iat)`
727
+ */
728
+ _checkToken() {
729
+ let token;
730
+ try {
731
+ token = this._storage.get(this._tokenKey);
732
+ } catch {
733
+ return;
734
+ }
735
+ if (!token) return;
736
+ const payload = this._decodeJwt(token);
737
+ if (!payload) return;
738
+ const { exp, iat } = payload;
739
+ if (exp == null || iat == null) return;
740
+ const now = Math.floor(Date.now() / 1e3);
741
+ if (exp - now < now - iat) {
742
+ void this.refreshSession();
743
+ }
744
+ }
745
+ /**
746
+ * Decode a JWT payload without verifying the signature.
747
+ *
748
+ * Security hardening:
749
+ * - Rejects payloads larger than 1 MB after decode (OOM protection)
750
+ * - Strips `__proto__` from parsed object (prototype pollution prevention)
751
+ *
752
+ * @returns Parsed payload or `null` on any failure.
753
+ */
754
+ _decodeJwt(token) {
755
+ try {
756
+ const parts = token.split(".");
757
+ if (parts.length !== 3) return null;
758
+ const payload = parts[1];
759
+ let base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
760
+ const padLength = (4 - base64.length % 4) % 4;
761
+ base64 += "=".repeat(padLength);
762
+ const decoded = atob(base64);
763
+ if (decoded.length > 1048576) return null;
764
+ const parsed = JSON.parse(decoded);
765
+ if ("__proto__" in parsed) {
766
+ delete parsed.__proto__;
767
+ }
768
+ return parsed;
769
+ } catch {
770
+ return null;
771
+ }
772
+ }
773
+ };
774
+ function _isUnauthorized(err) {
775
+ if (err == null || typeof err !== "object") return false;
776
+ const e = err;
777
+ if (e.response && typeof e.response === "object" && e.response.status === 401) return true;
778
+ if (e.status === 401) return true;
779
+ if (e.statusCode === 401) return true;
780
+ if (typeof e.message === "string" && /\b401\b/.test(e.message)) return true;
781
+ return false;
782
+ }
783
+ function _errorMessage(err) {
784
+ if (err instanceof Error) return err.message;
785
+ if (typeof err === "string") return err;
786
+ return "Unknown error";
787
+ }
788
+
789
+ // src/utils.ts
790
+ function encodeConnectUrl(url) {
791
+ if (url == null) {
792
+ throw new TypeError("encodeConnectUrl: input must not be null or undefined");
793
+ }
794
+ if (url === "") return "";
795
+ const base64 = btoa(unescape(encodeURIComponent(url)));
796
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
797
+ }
798
+ function decodeConnectUrl(encoded) {
799
+ if (encoded == null) {
800
+ throw new TypeError("decodeConnectUrl: input must not be null or undefined");
801
+ }
802
+ if (encoded === "") return "";
803
+ try {
804
+ let base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
805
+ const pad = base64.length % 4;
806
+ if (pad === 2) base64 += "==";
807
+ else if (pad === 3) base64 += "=";
808
+ else if (pad === 1) base64 += "===";
809
+ return decodeURIComponent(escape(atob(base64)));
810
+ } catch {
811
+ return "";
812
+ }
813
+ }
814
+ function parseTokenFromUrl(url, tokenKey) {
815
+ if (!url) return null;
816
+ const key = tokenKey || DEFAULTS.TOKEN_KEY;
817
+ try {
818
+ const parsed = new URL(url, "http://placeholder");
819
+ return parsed.searchParams.get(key) ?? null;
820
+ } catch {
821
+ return null;
822
+ }
823
+ }
824
+ function parseNextWorkflow(url, tokenKey) {
825
+ if (!url) return null;
826
+ const token = parseTokenFromUrl(url, tokenKey);
827
+ if (!token) return null;
828
+ try {
829
+ const parsed = new URL(url, "http://placeholder");
830
+ const key = tokenKey || DEFAULTS.TOKEN_KEY;
831
+ parsed.searchParams.delete(key);
832
+ const isAbsolute = /^https?:\/\//i.test(url);
833
+ const nextUrl = isAbsolute ? parsed.toString() : `${parsed.pathname}${parsed.search}`;
834
+ return { nextUrl, token };
835
+ } catch {
836
+ return null;
837
+ }
838
+ }
839
+ function getApiErrorMessage(err, defaultMsg) {
840
+ const fallback = defaultMsg || "Unknown error";
841
+ if (!err) return fallback;
842
+ try {
843
+ const data = err?.response?.data;
844
+ if (data) {
845
+ if (typeof data.error === "string" && data.error) return data.error;
846
+ if (typeof data.message === "string" && data.message) return data.message;
847
+ }
848
+ if (typeof err.message === "string" && err.message) return err.message;
849
+ } catch {
850
+ }
851
+ return fallback;
852
+ }
853
+ var WEBAUTHN_ERROR_MAP = {
854
+ NotAllowedError: "The operation was cancelled or timed out. Please try again.",
855
+ SecurityError: "The operation is not allowed in this context (check your domain or HTTPS settings).",
856
+ InvalidStateError: "This authenticator is already registered. Please use a different one.",
857
+ NotSupportedError: "This browser or device does not support the requested credential type.",
858
+ AbortError: "The operation was aborted.",
859
+ ConstraintError: "The authenticator does not meet the required constraints.",
860
+ UnknownError: "An unknown error occurred with the authenticator.",
861
+ NetworkError: "A network error occurred during the WebAuthn operation.",
862
+ DataError: "The provided data is invalid for a WebAuthn operation.",
863
+ TimeoutError: "The operation timed out. Please try again."
864
+ };
865
+ function getWebAuthnErrorMessage(err, defaultMsg) {
866
+ if (!err) return defaultMsg || "Unknown error";
867
+ const name = err.name;
868
+ if (typeof name === "string" && name in WEBAUTHN_ERROR_MAP) {
869
+ return WEBAUTHN_ERROR_MAP[name];
870
+ }
871
+ return getApiErrorMessage(err, defaultMsg);
872
+ }
873
+ function getBrowserLocale() {
874
+ try {
875
+ if (typeof navigator !== "undefined" && navigator.language) {
876
+ return navigator.language;
877
+ }
878
+ } catch {
879
+ }
880
+ return "en";
881
+ }
882
+ function openPopup(url, options) {
883
+ if (typeof window === "undefined" || typeof window.open !== "function") {
884
+ return null;
885
+ }
886
+ if (/^\s*javascript\s*:/i.test(url)) {
887
+ throw new Error("openPopup: javascript: URLs are not allowed");
888
+ }
889
+ const width = options?.width ?? 480;
890
+ const height = options?.height ?? 640;
891
+ const left = Math.round((window.screen.width - width) / 2);
892
+ const top = Math.round((window.screen.height - height) / 2);
893
+ const features = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes,status=yes`;
894
+ const popup = window.open(url, "_blank", features);
895
+ return popup ?? null;
896
+ }
897
+ function waitForPopupMessage(popup, options) {
898
+ const timeout = options?.timeout ?? 3e5;
899
+ const origin = options?.origin;
900
+ return new Promise((resolve, reject) => {
901
+ let timer;
902
+ let pollTimer;
903
+ function cleanup() {
904
+ if (typeof window !== "undefined") {
905
+ window.removeEventListener("message", onMessage);
906
+ }
907
+ if (timer !== void 0) clearTimeout(timer);
908
+ if (pollTimer !== void 0) clearInterval(pollTimer);
909
+ }
910
+ function onMessage(event) {
911
+ if (origin && event.origin !== origin) return;
912
+ if (event.source !== popup) return;
913
+ cleanup();
914
+ resolve(event.data);
915
+ }
916
+ if (typeof window === "undefined") {
917
+ reject(new Error("waitForPopupMessage: not in a browser environment"));
918
+ return;
919
+ }
920
+ window.addEventListener("message", onMessage);
921
+ timer = setTimeout(() => {
922
+ cleanup();
923
+ reject(new Error("waitForPopupMessage: timed out"));
924
+ }, timeout);
925
+ pollTimer = setInterval(() => {
926
+ try {
927
+ if (popup.closed) {
928
+ cleanup();
929
+ reject(new Error("waitForPopupMessage: popup was closed"));
930
+ }
931
+ } catch {
932
+ }
933
+ }, 500);
934
+ });
935
+ }
936
+ function buildActionPrefix(config) {
937
+ const apiPrefix = config.apiPrefix ?? DEFAULTS.API_PREFIX;
938
+ const action = config.action ?? DEFAULTS.ACTION;
939
+ const prefix = apiPrefix.replace(/\/+$/, "");
940
+ const act = action.replace(/^\/+/, "");
941
+ return `${prefix}/${act}`;
942
+ }
943
+ function stringifyParams(params) {
944
+ const parts = [];
945
+ const keys = Object.keys(params);
946
+ for (let i = 0; i < keys.length; i++) {
947
+ const key = keys[i];
948
+ const value = params[key];
949
+ if (value == null) continue;
950
+ if (Array.isArray(value)) {
951
+ for (let j = 0; j < value.length; j++) {
952
+ const item = value[j];
953
+ if (item == null) continue;
954
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`);
955
+ }
956
+ } else if (typeof value === "boolean") {
957
+ parts.push(`${encodeURIComponent(key)}=${value ? "true" : "false"}`);
958
+ } else {
959
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
960
+ }
961
+ }
962
+ return parts.join("&");
963
+ }
964
+ function getConnectedInfo(storage) {
965
+ return {
966
+ did: storage.get(STORAGE_KEYS.CONNECTED_DID) ?? void 0,
967
+ pk: storage.get(STORAGE_KEYS.CONNECTED_PK) ?? void 0,
968
+ app: storage.get(STORAGE_KEYS.CONNECTED_APP) ?? void 0,
969
+ walletOS: storage.get(STORAGE_KEYS.CONNECTED_WALLET_OS) ?? void 0
970
+ };
971
+ }
972
+
973
+ // src/token-session.ts
974
+ function makeInitialState() {
975
+ return {
976
+ token: null,
977
+ url: null,
978
+ status: null,
979
+ error: null,
980
+ appInfo: null,
981
+ memberAppInfo: null,
982
+ loading: false
983
+ };
984
+ }
985
+ var TokenSession = class {
986
+ // -- State --
987
+ _state;
988
+ _config;
989
+ _http;
990
+ // -- Polling --
991
+ _pollTimer = null;
992
+ _checkCount = 0;
993
+ _maxCheckCount;
994
+ // -- Realtime --
995
+ _realtimeUnsub = null;
996
+ _realtime;
997
+ // -- Storage --
998
+ _storage;
999
+ // -- Throttle --
1000
+ _lastCreateTime = 0;
1001
+ // -- Hand-rolled EventEmitter --
1002
+ _listeners = /* @__PURE__ */ new Map();
1003
+ // -- Lifecycle --
1004
+ _destroyed = false;
1005
+ // --------------------------------------------------------------------------
1006
+ // Constructor
1007
+ // --------------------------------------------------------------------------
1008
+ constructor(config, http, options) {
1009
+ this._config = this._fillDefaults(config);
1010
+ this._http = http;
1011
+ this._realtime = options?.realtime;
1012
+ this._storage = options?.storage;
1013
+ this._state = makeInitialState();
1014
+ if (this._config.checkInterval <= 0) {
1015
+ this._maxCheckCount = Infinity;
1016
+ } else {
1017
+ this._maxCheckCount = Math.ceil(
1018
+ this._config.tokenTimeout / this._config.checkInterval
1019
+ );
1020
+ }
1021
+ }
1022
+ // --------------------------------------------------------------------------
1023
+ // Public getters
1024
+ // --------------------------------------------------------------------------
1025
+ /** Return a shallow read-only copy of the current state. */
1026
+ get state() {
1027
+ return Object.freeze({ ...this._state });
1028
+ }
1029
+ // --------------------------------------------------------------------------
1030
+ // Public methods
1031
+ // --------------------------------------------------------------------------
1032
+ /**
1033
+ * Create a new connect token via the server.
1034
+ *
1035
+ * Throttled: calls within 500 ms of each other are silently ignored.
1036
+ * Any active polling is stopped before the request is made.
1037
+ */
1038
+ async create(params) {
1039
+ if (Date.now() - this._lastCreateTime < DEFAULTS.CREATE_THROTTLE) {
1040
+ return this.state;
1041
+ }
1042
+ if (this._pollTimer !== null || this._realtimeUnsub !== null) {
1043
+ this.stopPolling();
1044
+ }
1045
+ this._state.loading = true;
1046
+ this._emit("statusChange", this.state);
1047
+ const url = `${buildActionPrefix(this._config)}/token`;
1048
+ const queryParams = {};
1049
+ if (params) {
1050
+ if (params.locale != null) queryParams.locale = params.locale;
1051
+ if (params.provider != null) {
1052
+ queryParams.provider = params.provider;
1053
+ } else {
1054
+ queryParams.provider = "wallet";
1055
+ }
1056
+ if (params.encKey != null) queryParams.__encKey = params.encKey;
1057
+ if (params.autoConnect != null) queryParams.autoConnect = params.autoConnect;
1058
+ if (params.visitorId != null) queryParams.visitorId = params.visitorId;
1059
+ if (params.sourceToken != null) queryParams.sourceToken = params.sourceToken;
1060
+ if (params.forceConnected != null) queryParams.forceConnected = params.forceConnected;
1061
+ if (params.connectUrl != null) queryParams.connectUrl = params.connectUrl;
1062
+ if (params.extraParams) {
1063
+ Object.assign(queryParams, params.extraParams);
1064
+ }
1065
+ } else {
1066
+ queryParams.provider = "wallet";
1067
+ }
1068
+ try {
1069
+ const response = await this._http.get(url, {
1070
+ params: queryParams
1071
+ });
1072
+ this._state.token = response.token ?? null;
1073
+ this._state.url = response.url ?? null;
1074
+ this._state.status = TOKEN_STATUS.CREATED;
1075
+ this._state.appInfo = response.appInfo ?? null;
1076
+ this._state.memberAppInfo = response.memberAppInfo ?? null;
1077
+ this._state.loading = false;
1078
+ this._state.error = null;
1079
+ this._state.successResult = void 0;
1080
+ this._state.connectedDid = void 0;
1081
+ this._state.mfaCode = void 0;
1082
+ this._state.saveConnect = void 0;
1083
+ this._lastCreateTime = Date.now();
1084
+ this._emit("statusChange", this.state);
1085
+ return this.state;
1086
+ } catch (err) {
1087
+ this._state.loading = false;
1088
+ this._state.error = err?.message ?? "Failed to create token";
1089
+ this._state.status = TOKEN_STATUS.ERROR;
1090
+ this._emit("statusChange", this.state);
1091
+ this._emit("error", new ConnectError(ERROR_CODES.NETWORK_ERROR, this._state.error));
1092
+ throw err;
1093
+ }
1094
+ }
1095
+ /**
1096
+ * Check the current token status against the server.
1097
+ *
1098
+ * Throws if no token has been created yet.
1099
+ */
1100
+ async checkStatus() {
1101
+ if (!this._state.token) {
1102
+ throw new ConnectError(
1103
+ ERROR_CODES.TOKEN_NOT_FOUND,
1104
+ "No token available. Call create() first."
1105
+ );
1106
+ }
1107
+ const url = `${buildActionPrefix(this._config)}/status`;
1108
+ const params = {
1109
+ [DEFAULTS.TOKEN_KEY]: this._state.token
1110
+ };
1111
+ if (this._config.appPid) {
1112
+ }
1113
+ try {
1114
+ const response = await this._http.get(url, { params });
1115
+ const status = response.status;
1116
+ if (status) this._state.status = status;
1117
+ if (response.error !== void 0) this._state.error = response.error;
1118
+ if (response.connectedDid !== void 0) this._state.connectedDid = response.connectedDid;
1119
+ if (response.mfaCode !== void 0) this._state.mfaCode = response.mfaCode;
1120
+ if (response.saveConnect !== void 0) this._state.saveConnect = response.saveConnect;
1121
+ if (status === TOKEN_STATUS.FORBIDDEN) {
1122
+ this._state.status = TOKEN_STATUS.ERROR;
1123
+ this._state.error = response.error || "Access forbidden";
1124
+ this._emit("error", new ConnectError(ERROR_CODES.UNAUTHORIZED, this._state.error));
1125
+ this.stopPolling();
1126
+ } else if (status === TOKEN_STATUS.SUCCEED) {
1127
+ if (this._config.tokenDelivery === "response") {
1128
+ this._state.successResult = response;
1129
+ }
1130
+ this._emit("succeed", this.state);
1131
+ this.stopPolling();
1132
+ } else if (status === TOKEN_STATUS.ERROR) {
1133
+ this._emit(
1134
+ "error",
1135
+ new ConnectError(ERROR_CODES.INVALID_RESPONSE, this._state.error || "Unknown error")
1136
+ );
1137
+ this.stopPolling();
1138
+ } else if (status === TOKEN_STATUS.TIMEOUT) {
1139
+ this._emit("timeout", this.state);
1140
+ this.stopPolling();
1141
+ }
1142
+ this._emit("statusChange", this.state);
1143
+ return this.state;
1144
+ } catch (err) {
1145
+ throw err;
1146
+ }
1147
+ }
1148
+ /**
1149
+ * Cancel the current token by notifying the server, then stop polling.
1150
+ *
1151
+ * Silently returns if there is no active token.
1152
+ */
1153
+ async cancel() {
1154
+ if (!this._state.token) return;
1155
+ const url = `${buildActionPrefix(this._config)}/timeout`;
1156
+ const params = {
1157
+ [DEFAULTS.TOKEN_KEY]: this._state.token
1158
+ };
1159
+ try {
1160
+ await this._http.get(url, { params });
1161
+ } catch {
1162
+ }
1163
+ this.stopPolling();
1164
+ }
1165
+ /**
1166
+ * Begin polling for status updates.
1167
+ *
1168
+ * If a RealtimeAdapter was provided, subscribes to the realtime channel
1169
+ * instead of interval polling. Falls back to interval on subscription error.
1170
+ */
1171
+ startPolling() {
1172
+ if (!this._state.token) return;
1173
+ this._checkCount = 0;
1174
+ if (this._realtime) {
1175
+ try {
1176
+ const channel = `relay:${this._config.appPid}:${this._state.token}`;
1177
+ this._realtimeUnsub = this._realtime.subscribe(channel, (data) => {
1178
+ this._handleRealtimeMessage(data);
1179
+ });
1180
+ return;
1181
+ } catch {
1182
+ this._realtimeUnsub = null;
1183
+ }
1184
+ }
1185
+ if (this._config.checkInterval > 0) {
1186
+ this._pollTimer = setInterval(() => {
1187
+ this._pollOnce();
1188
+ }, this._config.checkInterval);
1189
+ }
1190
+ }
1191
+ /**
1192
+ * Stop all polling — clears interval timer and realtime subscription.
1193
+ */
1194
+ stopPolling() {
1195
+ if (this._pollTimer !== null) {
1196
+ clearInterval(this._pollTimer);
1197
+ this._pollTimer = null;
1198
+ }
1199
+ if (this._realtimeUnsub) {
1200
+ this._realtimeUnsub();
1201
+ this._realtimeUnsub = null;
1202
+ }
1203
+ }
1204
+ /**
1205
+ * Reset the session to its initial state.
1206
+ *
1207
+ * Stops polling and clears all token data, but preserves event listeners.
1208
+ */
1209
+ reset() {
1210
+ this.stopPolling();
1211
+ this._state = makeInitialState();
1212
+ this._checkCount = 0;
1213
+ }
1214
+ /**
1215
+ * Permanently tear down this session.
1216
+ *
1217
+ * Idempotent — subsequent calls are no-ops. Clears listeners, stops polling,
1218
+ * and nulls out token data.
1219
+ */
1220
+ destroy() {
1221
+ if (this._destroyed) return;
1222
+ this.stopPolling();
1223
+ this._state.token = null;
1224
+ this._state.successResult = void 0;
1225
+ this._listeners.clear();
1226
+ this._destroyed = true;
1227
+ }
1228
+ // --------------------------------------------------------------------------
1229
+ // Event methods (hand-rolled EventEmitter)
1230
+ // --------------------------------------------------------------------------
1231
+ /**
1232
+ * Register an event handler. Returns `this` for chaining.
1233
+ *
1234
+ * Events:
1235
+ * - `statusChange` — fired on every state mutation, payload: `TokenState`
1236
+ * - `succeed` — token auth completed successfully, payload: `TokenState`
1237
+ * - `error` — an error occurred, payload: `ConnectError`
1238
+ * - `timeout` — token or poll timed out, payload: `TokenState`
1239
+ */
1240
+ on(event, handler) {
1241
+ let set = this._listeners.get(event);
1242
+ if (!set) {
1243
+ set = /* @__PURE__ */ new Set();
1244
+ this._listeners.set(event, set);
1245
+ }
1246
+ set.add(handler);
1247
+ return this;
1248
+ }
1249
+ /**
1250
+ * Remove an event handler. Returns `this` for chaining.
1251
+ */
1252
+ off(event, handler) {
1253
+ const set = this._listeners.get(event);
1254
+ if (set) {
1255
+ set.delete(handler);
1256
+ if (set.size === 0) {
1257
+ this._listeners.delete(event);
1258
+ }
1259
+ }
1260
+ return this;
1261
+ }
1262
+ // --------------------------------------------------------------------------
1263
+ // Private methods
1264
+ // --------------------------------------------------------------------------
1265
+ /** Emit an event to all registered handlers. */
1266
+ _emit(event, ...args) {
1267
+ const set = this._listeners.get(event);
1268
+ if (!set || set.size === 0) return;
1269
+ for (const handler of [...set]) {
1270
+ try {
1271
+ handler(...args);
1272
+ } catch {
1273
+ }
1274
+ }
1275
+ }
1276
+ /** Single poll tick — called by setInterval. */
1277
+ _pollOnce() {
1278
+ this._checkCount++;
1279
+ if (this._checkCount >= this._maxCheckCount) {
1280
+ this._handleTimeout();
1281
+ return;
1282
+ }
1283
+ this.checkStatus().catch(() => {
1284
+ });
1285
+ }
1286
+ /** Handle poll timeout (max iterations reached). */
1287
+ _handleTimeout() {
1288
+ this.stopPolling();
1289
+ this._state.status = TOKEN_STATUS.TIMEOUT;
1290
+ this._emit("timeout", this.state);
1291
+ this._emit("statusChange", this.state);
1292
+ }
1293
+ /** Handle an incoming realtime message for the subscribed token channel. */
1294
+ _handleRealtimeMessage(data) {
1295
+ if (!data || typeof data !== "object") return;
1296
+ const status = data.status;
1297
+ if (!status) return;
1298
+ this._state.status = status;
1299
+ if (data.error !== void 0) this._state.error = data.error;
1300
+ if (data.connectedDid !== void 0) this._state.connectedDid = data.connectedDid;
1301
+ if (data.mfaCode !== void 0) this._state.mfaCode = data.mfaCode;
1302
+ if (data.saveConnect !== void 0) this._state.saveConnect = data.saveConnect;
1303
+ if (status === TOKEN_STATUS.FORBIDDEN) {
1304
+ this._state.status = TOKEN_STATUS.ERROR;
1305
+ this._state.error = data.error || "Access forbidden";
1306
+ this._emit("error", new ConnectError(ERROR_CODES.UNAUTHORIZED, this._state.error));
1307
+ this.stopPolling();
1308
+ } else if (status === TOKEN_STATUS.SUCCEED) {
1309
+ if (this._config.tokenDelivery === "response") {
1310
+ this._state.successResult = data;
1311
+ }
1312
+ this._emit("succeed", this.state);
1313
+ this.stopPolling();
1314
+ } else if (status === TOKEN_STATUS.ERROR) {
1315
+ this._emit(
1316
+ "error",
1317
+ new ConnectError(ERROR_CODES.INVALID_RESPONSE, this._state.error || "Unknown error")
1318
+ );
1319
+ this.stopPolling();
1320
+ } else if (status === TOKEN_STATUS.TIMEOUT) {
1321
+ this._emit("timeout", this.state);
1322
+ this.stopPolling();
1323
+ }
1324
+ this._emit("statusChange", this.state);
1325
+ }
1326
+ /** Fill optional ConnectConfig fields with DEFAULTS. */
1327
+ _fillDefaults(config) {
1328
+ return {
1329
+ appPid: config.appPid,
1330
+ appName: config.appName,
1331
+ appDescription: config.appDescription ?? "",
1332
+ appIcon: config.appIcon ?? "",
1333
+ appUrl: config.appUrl ?? "",
1334
+ apiPrefix: config.apiPrefix ?? DEFAULTS.API_PREFIX,
1335
+ action: config.action ?? DEFAULTS.ACTION,
1336
+ servicePrefix: config.servicePrefix ?? DEFAULTS.SERVICE_PREFIX,
1337
+ tokenTimeout: config.tokenTimeout ?? DEFAULTS.TOKEN_TIMEOUT,
1338
+ checkInterval: config.checkInterval ?? DEFAULTS.CHECK_INTERVAL,
1339
+ tokenDelivery: config.tokenDelivery ?? DEFAULTS.TOKEN_DELIVERY
1340
+ };
1341
+ }
1342
+ };
1343
+
1344
+ // src/index.ts
1345
+ var VERSION = "4.0.0-beta.0";
1346
+ // Annotate the CommonJS export names for ESM import in node:
1347
+ 0 && (module.exports = {
1348
+ ConnectError,
1349
+ DEFAULTS,
1350
+ DID_PREFIX,
1351
+ ERROR_CODES,
1352
+ EmailFlow,
1353
+ FetchHttpAdapter,
1354
+ OAuthFlow,
1355
+ PasskeyFlow,
1356
+ STORAGE_KEYS,
1357
+ SessionManager,
1358
+ TERMINAL_STATUSES,
1359
+ TOKEN_STATUS,
1360
+ TokenSession,
1361
+ VERSION,
1362
+ buildActionPrefix,
1363
+ decodeConnectUrl,
1364
+ encodeConnectUrl,
1365
+ getApiErrorMessage,
1366
+ getBrowserLocale,
1367
+ getConnectedInfo,
1368
+ getDidChainLabel,
1369
+ getDidDisplayParts,
1370
+ getWebAuthnErrorMessage,
1371
+ isEthereumDid,
1372
+ openPopup,
1373
+ parseNextWorkflow,
1374
+ parseTokenFromUrl,
1375
+ stringifyParams,
1376
+ truncateDid,
1377
+ waitForPopupMessage
1378
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/did-connect-core",
3
- "version": "4.0.1",
3
+ "version": "4.0.2",
4
4
  "description": "Framework-agnostic DID Connect protocol client — types, state machines, adapters",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -8,7 +8,8 @@
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
12
13
  },
13
14
  "./ui": {
14
15
  "types": "./dist/ui/index.d.ts",
@@ -29,7 +30,7 @@
29
30
  "qrcode-generator": "^2.0.4"
30
31
  },
31
32
  "scripts": {
32
- "build": "tsc && node build-ui.mjs",
33
+ "build": "tsc && node build-ui.mjs && esbuild src/index.ts --bundle --format=cjs --platform=node --target=node20 --outfile=dist/index.cjs",
33
34
  "lint": "biome check src/",
34
35
  "test": "vitest run",
35
36
  "test:coverage": "vitest run --coverage",