@meshconnect/uwc-core 1.0.3 → 1.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.
Files changed (121) hide show
  1. package/README.md +247 -76
  2. package/dist/events.d.ts +241 -5
  3. package/dist/events.d.ts.map +1 -1
  4. package/dist/events.js +77 -1
  5. package/dist/events.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/logger/create-logger.d.ts +32 -0
  11. package/dist/logger/create-logger.d.ts.map +1 -0
  12. package/dist/logger/create-logger.js +79 -0
  13. package/dist/logger/create-logger.js.map +1 -0
  14. package/dist/managers/event-manager.d.ts.map +1 -1
  15. package/dist/managers/event-manager.js +5 -1
  16. package/dist/managers/event-manager.js.map +1 -1
  17. package/dist/observability/connect-observer.d.ts +33 -0
  18. package/dist/observability/connect-observer.d.ts.map +1 -0
  19. package/dist/observability/connect-observer.js +115 -0
  20. package/dist/observability/connect-observer.js.map +1 -0
  21. package/dist/observability/focus-tracker.d.ts +42 -0
  22. package/dist/observability/focus-tracker.d.ts.map +1 -0
  23. package/dist/observability/focus-tracker.js +57 -0
  24. package/dist/observability/focus-tracker.js.map +1 -0
  25. package/dist/observability/instrument-handoff.d.ts +54 -0
  26. package/dist/observability/instrument-handoff.d.ts.map +1 -0
  27. package/dist/observability/instrument-handoff.js +173 -0
  28. package/dist/observability/instrument-handoff.js.map +1 -0
  29. package/dist/observability/lifecycle-bridge.d.ts +69 -0
  30. package/dist/observability/lifecycle-bridge.d.ts.map +1 -0
  31. package/dist/observability/lifecycle-bridge.js +155 -0
  32. package/dist/observability/lifecycle-bridge.js.map +1 -0
  33. package/dist/observability/scrub.d.ts +31 -0
  34. package/dist/observability/scrub.d.ts.map +1 -0
  35. package/dist/observability/scrub.js +167 -0
  36. package/dist/observability/scrub.js.map +1 -0
  37. package/dist/observability/telemetry.d.ts +157 -0
  38. package/dist/observability/telemetry.d.ts.map +1 -0
  39. package/dist/observability/telemetry.js +373 -0
  40. package/dist/observability/telemetry.js.map +1 -0
  41. package/dist/services/connection-service.d.ts +23 -1
  42. package/dist/services/connection-service.d.ts.map +1 -1
  43. package/dist/services/connection-service.js +128 -19
  44. package/dist/services/connection-service.js.map +1 -1
  45. package/dist/services/network-switch-service.d.ts +2 -1
  46. package/dist/services/network-switch-service.d.ts.map +1 -1
  47. package/dist/services/network-switch-service.js +45 -16
  48. package/dist/services/network-switch-service.js.map +1 -1
  49. package/dist/services/signature-service.d.ts +14 -7
  50. package/dist/services/signature-service.d.ts.map +1 -1
  51. package/dist/services/signature-service.js +56 -58
  52. package/dist/services/signature-service.js.map +1 -1
  53. package/dist/services/transaction-service.d.ts +19 -5
  54. package/dist/services/transaction-service.d.ts.map +1 -1
  55. package/dist/services/transaction-service.js +55 -38
  56. package/dist/services/transaction-service.js.map +1 -1
  57. package/dist/universal-wallet-connector.d.ts +56 -1
  58. package/dist/universal-wallet-connector.d.ts.map +1 -1
  59. package/dist/universal-wallet-connector.js +207 -110
  60. package/dist/universal-wallet-connector.js.map +1 -1
  61. package/dist/utils/id.d.ts +2 -0
  62. package/dist/utils/id.d.ts.map +1 -0
  63. package/dist/utils/id.js +41 -0
  64. package/dist/utils/id.js.map +1 -0
  65. package/dist/utils/resolve-session-provider.d.ts +10 -0
  66. package/dist/utils/resolve-session-provider.d.ts.map +1 -0
  67. package/dist/utils/resolve-session-provider.js +21 -0
  68. package/dist/utils/resolve-session-provider.js.map +1 -0
  69. package/dist/utils/to-wallet-error.d.ts +35 -0
  70. package/dist/utils/to-wallet-error.d.ts.map +1 -0
  71. package/dist/utils/to-wallet-error.js +130 -0
  72. package/dist/utils/to-wallet-error.js.map +1 -0
  73. package/dist/utils/validate-wallet-session.d.ts +15 -0
  74. package/dist/utils/validate-wallet-session.d.ts.map +1 -0
  75. package/dist/utils/validate-wallet-session.js +29 -0
  76. package/dist/utils/validate-wallet-session.js.map +1 -0
  77. package/dist/version.d.ts +2 -0
  78. package/dist/version.d.ts.map +1 -0
  79. package/dist/version.js +3 -0
  80. package/dist/version.js.map +1 -0
  81. package/package.json +6 -6
  82. package/src/events.ts +326 -14
  83. package/src/index.ts +10 -0
  84. package/src/logger/create-logger.test.ts +126 -0
  85. package/src/logger/create-logger.ts +112 -0
  86. package/src/managers/event-manager.test.ts +55 -0
  87. package/src/managers/event-manager.ts +5 -1
  88. package/src/observability/connect-observer.test.ts +214 -0
  89. package/src/observability/connect-observer.ts +128 -0
  90. package/src/observability/focus-tracker.test.ts +191 -0
  91. package/src/observability/focus-tracker.ts +96 -0
  92. package/src/observability/instrument-handoff.test.ts +397 -0
  93. package/src/observability/instrument-handoff.ts +231 -0
  94. package/src/observability/lifecycle-bridge.test.ts +398 -0
  95. package/src/observability/lifecycle-bridge.ts +216 -0
  96. package/src/observability/scrub.test.ts +183 -0
  97. package/src/observability/scrub.ts +174 -0
  98. package/src/observability/telemetry.test.ts +549 -0
  99. package/src/observability/telemetry.ts +546 -0
  100. package/src/services/connection-service.test.ts +261 -1
  101. package/src/services/connection-service.ts +167 -38
  102. package/src/services/network-switch-service.test.ts +51 -1
  103. package/src/services/network-switch-service.ts +56 -24
  104. package/src/services/signature-service.test.ts +181 -76
  105. package/src/services/signature-service.ts +75 -85
  106. package/src/services/transaction-service.test.ts +270 -69
  107. package/src/services/transaction-service.ts +74 -51
  108. package/src/universal-wallet-connector.instrumentation-guard.test.ts +199 -0
  109. package/src/universal-wallet-connector.observability.test.ts +610 -0
  110. package/src/universal-wallet-connector.test.ts +27 -146
  111. package/src/universal-wallet-connector.ts +279 -142
  112. package/src/utils/id.test.ts +15 -0
  113. package/src/utils/id.ts +48 -0
  114. package/src/utils/resolve-session-provider.test.ts +54 -0
  115. package/src/utils/resolve-session-provider.ts +25 -0
  116. package/src/utils/to-wallet-error.test.ts +181 -0
  117. package/src/utils/to-wallet-error.ts +152 -0
  118. package/src/utils/validate-wallet-session.test.ts +77 -0
  119. package/src/utils/validate-wallet-session.ts +40 -0
  120. package/src/version.test.ts +11 -0
  121. package/src/version.ts +2 -0
@@ -0,0 +1,115 @@
1
+ import { createBoundedDedup, safeErrorMessage } from '@meshconnect/uwc-types';
2
+ import { UWC_EVENT_NAMES } from '../events';
3
+ import { normalizeEvent } from './telemetry';
4
+ import { scrubArgs, scrubMessage } from './scrub';
5
+ /**
6
+ * Bridge the internal event bus to a consumer-supplied `UWCObserver`.
7
+ *
8
+ * Subscribes to every `UWCEventMap` event, normalizes each to a PII-safe
9
+ * `UWCTelemetryEvent`, and forwards it to `observer.onEvent`. The `log` event is
10
+ * routed to `observer.onLog` instead (with scrubbed args) — it's the logging
11
+ * channel, not a telemetry record. `change` is skipped: it's the legacy
12
+ * re-render cascade and carries no payload.
13
+ *
14
+ * Returns a teardown function that removes every subscription. The caller MUST
15
+ * invoke it on connector teardown — leaving these attached is the same listener-
16
+ * leak class as the historical `BridgeParent.destroy()` zombie bug.
17
+ *
18
+ * @param sdkSessionId - per-connector-instance id stamped on every record.
19
+ * @param getCorrelationId - resolves the optional consumer correlation id,
20
+ * called per-event so an id that arrives/changes after construction is
21
+ * honoured. Must not throw (the caller pre-wraps it). Defaults to `undefined`.
22
+ * @param now - clock injectable for deterministic tests; defaults to `Date.now`.
23
+ * @param logger - optional; a throwing observer is reported via `logger.debug`.
24
+ * When omitted the throw is swallowed silently (the historical behaviour).
25
+ */
26
+ /**
27
+ * Cap on distinct observer-error messages remembered for dedup. Same storm-guard
28
+ * contract as the relay/bridge dedup sets: bounding keeps memory flat on a
29
+ * long-lived instance even when a broken observer throws high-cardinality
30
+ * messages (e.g. embedding event data or timestamps).
31
+ */
32
+ export const MAX_REPORTED_OBSERVER_ERRORS = 100;
33
+ export function connectObserver(eventManager, observer, sdkSessionId, getCorrelationId = () => undefined, now = Date.now, logger) {
34
+ const unsubscribers = [];
35
+ // Set while we log an observer failure so the resulting `logger.debug(...)` —
36
+ // which re-enters the bus as a `log` event — is NOT forwarded back to the same
37
+ // (throwing) observer, which would recurse. One logical dispatch at a time.
38
+ let loggingObserverError = false;
39
+ // "Once per error site": dedupe by the error's message so a wallet whose
40
+ // observer throws on every event logs once, not on a tight loop. Scoped to THIS
41
+ // connector instance (not module-global) so one connector's failures don't
42
+ // suppress another's; bounded so it can't grow for the instance's lifetime.
43
+ const observerErrorDedup = createBoundedDedup(MAX_REPORTED_OBSERVER_ERRORS);
44
+ const reportObserverError = (err) => {
45
+ if (!logger)
46
+ return;
47
+ // safeErrorMessage: a poisoned getter / throwing toString on the observer's
48
+ // own error must not escape this reporter, and the dedup key stays bounded.
49
+ const key = safeErrorMessage(err);
50
+ if (!observerErrorDedup.firstSeen(key))
51
+ return;
52
+ loggingObserverError = true;
53
+ try {
54
+ logger.debug('UWC observer threw', err);
55
+ }
56
+ finally {
57
+ loggingObserverError = false;
58
+ }
59
+ };
60
+ for (const name of UWC_EVENT_NAMES) {
61
+ if (name === 'change')
62
+ continue;
63
+ if (name === 'log') {
64
+ unsubscribers.push(eventManager.on('log', data => {
65
+ if (!observer.onLog)
66
+ return;
67
+ // The observer-error debug log re-enters here as a `log` event; drop it
68
+ // so we never forward our own failure notice back to the bad observer.
69
+ if (loggingObserverError)
70
+ return;
71
+ const { level, message, args } = data;
72
+ try {
73
+ // Scrub the message too, not just args: the message reaches the same
74
+ // consumer sink, so the egress contract can't rely on every internal
75
+ // log string staying free of dynamic (potentially sensitive) data.
76
+ observer.onLog(level, scrubMessage(message), scrubArgs(args));
77
+ }
78
+ catch (err) {
79
+ // A throwing observer must not break the dispatch chain.
80
+ reportObserverError(err);
81
+ }
82
+ }));
83
+ continue;
84
+ }
85
+ unsubscribers.push(eventManager.on(name, data => {
86
+ if (!observer.onEvent)
87
+ return;
88
+ // Resolve the id in its OWN guard: a throwing getter is a config
89
+ // problem, not an observer failure — degrade to "no id" (the getter's
90
+ // documented contract) instead of dropping the record.
91
+ let correlationId;
92
+ try {
93
+ correlationId = getCorrelationId();
94
+ }
95
+ catch {
96
+ correlationId = undefined;
97
+ }
98
+ try {
99
+ // Normalize OUTSIDE the EventManager's own try/catch (which guards the
100
+ // listener, not the work the listener does), then hand off.
101
+ observer.onEvent(normalizeEvent(name, data, sdkSessionId, now(), correlationId));
102
+ }
103
+ catch (err) {
104
+ // Same: never let a bad observer break other listeners.
105
+ reportObserverError(err);
106
+ }
107
+ }));
108
+ }
109
+ return () => {
110
+ for (const unsubscribe of unsubscribers)
111
+ unsubscribe();
112
+ unsubscribers.length = 0;
113
+ };
114
+ }
115
+ //# sourceMappingURL=connect-observer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect-observer.js","sourceRoot":"","sources":["../../src/observability/connect-observer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import type { Platform } from '@meshconnect/uwc-types';
2
+ import type { EventManager } from '../managers/event-manager';
3
+ /**
4
+ * Minimal `document` surface this tracker touches. Declared locally (rather than
5
+ * relying on the DOM lib) so the module is SSR-safe and trivially testable with a
6
+ * fake — no jsdom required.
7
+ */
8
+ export interface FocusDocumentLike {
9
+ visibilityState?: string;
10
+ hidden?: boolean;
11
+ addEventListener(type: string, listener: () => void): void;
12
+ removeEventListener(type: string, listener: () => void): void;
13
+ }
14
+ /**
15
+ * Minimal `window` surface for the `pageshow` listener. Separate from
16
+ * `FocusDocumentLike` because the two events have different targets:
17
+ * `visibilitychange` fires on `document`, while `pageshow` is a WINDOW event
18
+ * (HTML spec / MDN: "sent to a Window") — a document-level listener would never
19
+ * fire in a real browser.
20
+ */
21
+ export interface FocusWindowLike {
22
+ addEventListener(type: string, listener: () => void): void;
23
+ removeEventListener(type: string, listener: () => void): void;
24
+ }
25
+ /**
26
+ * While a wallet handoff is pending, emit a FACT on every tab-visibility
27
+ * transition — `pageHiddenDuringHandoff` when the page is backgrounded (the user
28
+ * likely left for the wallet app), `pageVisibleDuringHandoff` when it returns
29
+ * (including a bfcache `pageshow`, which iOS uses when coming back from a wallet).
30
+ *
31
+ * This is the raw signal behind "did the user actually app-switch to the wallet?"
32
+ * for deep-link flows — it does NOT interpret abandonment. The listeners are
33
+ * scoped to the single handoff: attach right after `awaitingWallet`, and the
34
+ * returned teardown (called the instant the op settles) removes them, so the
35
+ * page's visibility is only observed during the blind window. SSR-safe (no-op
36
+ * when there is neither `document` nor `window`).
37
+ *
38
+ * @returns a teardown that removes the listeners. Always call it (even when
39
+ * nothing was attached — it's a no-op then) to keep the call site symmetric.
40
+ */
41
+ export declare function trackFocusDuringHandoff(eventManager: EventManager, handoffId: string, platform: Platform, doc?: FocusDocumentLike | undefined, win?: FocusWindowLike | undefined): () => void;
42
+ //# sourceMappingURL=focus-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-tracker.d.ts","sourceRoot":"","sources":["../../src/observability/focus-tracker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE7D;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAC1D,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;CAC9D;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAC1D,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;CAC9D;AAYD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,YAAY,EAC1B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,QAAQ,EAClB,GAAG,GAAE,iBAAiB,GAAG,SAA6B,EACtD,GAAG,GAAE,eAAe,GAAG,SAA2B,GACjD,MAAM,IAAI,CAoCZ"}
@@ -0,0 +1,57 @@
1
+ function resolveDocument() {
2
+ const doc = globalThis.document;
3
+ return doc && typeof doc.addEventListener === 'function' ? doc : undefined;
4
+ }
5
+ function resolveWindow() {
6
+ const win = globalThis.window;
7
+ return win && typeof win.addEventListener === 'function' ? win : undefined;
8
+ }
9
+ /**
10
+ * While a wallet handoff is pending, emit a FACT on every tab-visibility
11
+ * transition — `pageHiddenDuringHandoff` when the page is backgrounded (the user
12
+ * likely left for the wallet app), `pageVisibleDuringHandoff` when it returns
13
+ * (including a bfcache `pageshow`, which iOS uses when coming back from a wallet).
14
+ *
15
+ * This is the raw signal behind "did the user actually app-switch to the wallet?"
16
+ * for deep-link flows — it does NOT interpret abandonment. The listeners are
17
+ * scoped to the single handoff: attach right after `awaitingWallet`, and the
18
+ * returned teardown (called the instant the op settles) removes them, so the
19
+ * page's visibility is only observed during the blind window. SSR-safe (no-op
20
+ * when there is neither `document` nor `window`).
21
+ *
22
+ * @returns a teardown that removes the listeners. Always call it (even when
23
+ * nothing was attached — it's a no-op then) to keep the call site symmetric.
24
+ */
25
+ export function trackFocusDuringHandoff(eventManager, handoffId, platform, doc = resolveDocument(), win = resolveWindow()) {
26
+ if (!doc && !win)
27
+ return () => { };
28
+ // One physical return can fire BOTH `pageshow` (window) and a
29
+ // `visibilitychange`→visible (document) — an iOS bfcache restore does. Dedupe
30
+ // consecutive same-state emits so the hidden/visible events actually pair.
31
+ let lastState;
32
+ const emitState = (state) => {
33
+ if (state === lastState)
34
+ return;
35
+ lastState = state;
36
+ eventManager.emit(state === 'hidden'
37
+ ? 'pageHiddenDuringHandoff'
38
+ : 'pageVisibleDuringHandoff', { handoffId, platform });
39
+ };
40
+ const onVisibility = () => {
41
+ const hidden = doc?.visibilityState === 'hidden' || doc?.hidden === true;
42
+ emitState(hidden ? 'hidden' : 'visible');
43
+ };
44
+ // `pageshow` fires on a bfcache restore (returning from the wallet app on iOS),
45
+ // a "came back" signal `visibilitychange` doesn't always deliver. It targets
46
+ // the WINDOW — attaching it to `document` would silently never fire.
47
+ const onPageShow = () => {
48
+ emitState('visible');
49
+ };
50
+ doc?.addEventListener('visibilitychange', onVisibility);
51
+ win?.addEventListener('pageshow', onPageShow);
52
+ return () => {
53
+ doc?.removeEventListener('visibilitychange', onVisibility);
54
+ win?.removeEventListener('pageshow', onPageShow);
55
+ };
56
+ }
57
+ //# sourceMappingURL=focus-tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-tracker.js","sourceRoot":"","sources":["../../src/observability/focus-tracker.ts"],"names":[],"mappings":"AA2BA,SAAS,eAAe;IACtB,MAAM,GAAG,GAAI,UAA+C,CAAC,QAAQ,CAAA;IACrE,OAAO,GAAG,IAAI,OAAO,GAAG,CAAC,gBAAgB,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;AAC5E,CAAC;AAED,SAAS,aAAa;IACpB,MAAM,GAAG,GAAI,UAA2C,CAAC,MAAM,CAAA;IAC/D,OAAO,GAAG,IAAI,OAAO,GAAG,CAAC,gBAAgB,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAA;AAC5E,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,uBAAuB,CACrC,YAA0B,EAC1B,SAAiB,EACjB,QAAkB,EAClB,MAAqC,eAAe,EAAE,EACtD,MAAmC,aAAa,EAAE;IAElD,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG;QAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;IAEjC,8DAA8D;IAC9D,8EAA8E;IAC9E,2EAA2E;IAC3E,IAAI,SAA2C,CAAA;IAC/C,MAAM,SAAS,GAAG,CAAC,KAA2B,EAAE,EAAE;QAChD,IAAI,KAAK,KAAK,SAAS;YAAE,OAAM;QAC/B,SAAS,GAAG,KAAK,CAAA;QACjB,YAAY,CAAC,IAAI,CACf,KAAK,KAAK,QAAQ;YAChB,CAAC,CAAC,yBAAyB;YAC3B,CAAC,CAAC,0BAA0B,EAC9B,EAAE,SAAS,EAAE,QAAQ,EAAE,CACxB,CAAA;IACH,CAAC,CAAA;IAED,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,MAAM,MAAM,GAAG,GAAG,EAAE,eAAe,KAAK,QAAQ,IAAI,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;QACxE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;IAC1C,CAAC,CAAA;IACD,gFAAgF;IAChF,6EAA6E;IAC7E,qEAAqE;IACrE,MAAM,UAAU,GAAG,GAAG,EAAE;QACtB,SAAS,CAAC,SAAS,CAAC,CAAA;IACtB,CAAC,CAAA;IAED,GAAG,EAAE,gBAAgB,CAAC,kBAAkB,EAAE,YAAY,CAAC,CAAA;IACvD,GAAG,EAAE,gBAAgB,CAAC,UAAU,EAAE,UAAU,CAAC,CAAA;IAE7C,OAAO,GAAG,EAAE;QACV,GAAG,EAAE,mBAAmB,CAAC,kBAAkB,EAAE,YAAY,CAAC,CAAA;QAC1D,GAAG,EAAE,mBAAmB,CAAC,UAAU,EAAE,UAAU,CAAC,CAAA;IAClD,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,54 @@
1
+ import type { ConnectionMode, Namespace, NetworkId, Session } from '@meshconnect/uwc-types';
2
+ import type { EventManager } from '../managers/event-manager';
3
+ import type { UWCOperation } from '../events';
4
+ /** Routing facets known at the moment of a wallet handoff. No address, no payload. */
5
+ export interface HandoffContext {
6
+ operation: UWCOperation;
7
+ connectionMode: ConnectionMode;
8
+ walletId?: string | undefined;
9
+ namespace?: Namespace | undefined;
10
+ chainId?: NetworkId | undefined;
11
+ /** switchNetwork only: target chain already the active one (no-op switch). */
12
+ targetAlreadyActive?: boolean | undefined;
13
+ }
14
+ /** Build handoff routing facets from the active session (no address). */
15
+ export declare function sessionHandoffContext(operation: UWCOperation, session: Session): HandoffContext;
16
+ /** Whether this thrown value already produced a handoff terminal. */
17
+ export declare function hasHandoffTerminal(error: unknown): boolean;
18
+ export interface InstrumentHandoffOptions {
19
+ eventManager: EventManager;
20
+ /**
21
+ * Deadline after which `walletTimedOut` fires as a SIDE marker (never rejects).
22
+ * `0` disables the marker. `tonConnect` is always skipped — TON bounds its own
23
+ * wallet response internally, so a second observational timer would be noise.
24
+ */
25
+ timeoutMs: number;
26
+ /**
27
+ * The caller's cancellation signal, when the op accepts one. Consulted in the
28
+ * catch path: a rejection observed while the signal is aborted is treated as
29
+ * the cancellation itself (no terminal) even when the rejection value is a
30
+ * custom `abort(reason)` — which is NOT an AbortError by name.
31
+ */
32
+ signal?: AbortSignal | undefined;
33
+ /** Clock injectable for deterministic tests. */
34
+ now?: () => number;
35
+ }
36
+ /**
37
+ * Instrument one wallet-boundary op so the blind window is measured.
38
+ *
39
+ * Emits `awaitingWallet` immediately (before the await that hands control to the
40
+ * wallet), then exactly one terminal — `walletSucceeded` (success),
41
+ * `walletRejected` (explicit user no), or `walletFailed` (any other error) — all
42
+ * carrying the same `handoffId` and a `durationMs`. An intentional abort emits no
43
+ * terminal (the caller cancelled; it isn't a wallet outcome) — services check
44
+ * their signal AFTER this wrapper returns, so a wallet op that settled before
45
+ * the abort still records its truthful terminal first.
46
+ *
47
+ * A non-cancelling `walletTimedOut` marker fires if the deadline passes while the
48
+ * op is still pending; it is cleared the instant the op settles, so a fast op
49
+ * never false-fires it, and it NEVER rejects/aborts/mutates state — the wrapped
50
+ * promise stays the single terminal source of truth. This is a deliberate
51
+ * deviation from TON's cancelling `withWalletResponseTimeout`.
52
+ */
53
+ export declare function instrumentHandoff<T>(options: InstrumentHandoffOptions, context: HandoffContext, fn: () => Promise<T>): Promise<T>;
54
+ //# sourceMappingURL=instrument-handoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instrument-handoff.d.ts","sourceRoot":"","sources":["../../src/observability/instrument-handoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,SAAS,EACT,SAAS,EACT,OAAO,EACR,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAW7C,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,YAAY,CAAA;IACvB,cAAc,EAAE,cAAc,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,SAAS,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IACjC,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IAC/B,8EAA8E;IAC9E,mBAAmB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC1C;AAED,yEAAyE;AACzE,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,OAAO,GACf,cAAc,CAQhB;AAmBD,qEAAqE;AACrE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAM1D;AAQD,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,YAAY,CAAA;IAC1B;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;IAChC,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,wBAAwB,EACjC,OAAO,EAAE,cAAc,EACvB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAmHZ"}
@@ -0,0 +1,173 @@
1
+ import { generateId } from '../utils/id';
2
+ import { classifyError, isAbortError, isUserRejection, toWalletError } from '../utils/to-wallet-error';
3
+ import { derivePlatform, deriveWalletFlowType } from './telemetry';
4
+ import { trackFocusDuringHandoff } from './focus-tracker';
5
+ /** Build handoff routing facets from the active session (no address). */
6
+ export function sessionHandoffContext(operation, session) {
7
+ return {
8
+ operation,
9
+ connectionMode: session.connectionMode ?? 'injected',
10
+ walletId: session.activeWallet?.id,
11
+ namespace: session.activeNetwork?.namespace,
12
+ chainId: session.activeNetwork?.id ?? undefined
13
+ };
14
+ }
15
+ /**
16
+ * Errors that already produced a funnel terminal (`walletRejected` /
17
+ * `walletFailed`). `emitError` reads this to tag its back-compat `error` record
18
+ * as `handoffCovered`, so a dashboard can count wallet failures from terminals
19
+ * and pre-wallet failures from `error` without double-counting. A WeakSet keyed
20
+ * on the error object — no mutation of the thrown value, and entries die with
21
+ * the error.
22
+ *
23
+ * DOCUMENTED GAP: a WeakSet cannot hold primitives. A wallet that rejects with
24
+ * a bare string/number still produces its terminal, but the façade's `error`
25
+ * record can't be marked — that failure double-counts under the "terminals +
26
+ * uncovered `error`" metric. Accepted: EIP-1193 requires object errors, and
27
+ * wrapping the thrown value to make it markable would mutate what the caller
28
+ * receives.
29
+ */
30
+ const handoffTerminalErrors = new WeakSet();
31
+ /** Whether this thrown value already produced a handoff terminal. */
32
+ export function hasHandoffTerminal(error) {
33
+ return (typeof error === 'object' &&
34
+ error !== null &&
35
+ handoffTerminalErrors.has(error));
36
+ }
37
+ function markHandoffTerminal(error) {
38
+ if (typeof error === 'object' && error !== null) {
39
+ handoffTerminalErrors.add(error);
40
+ }
41
+ }
42
+ /**
43
+ * Instrument one wallet-boundary op so the blind window is measured.
44
+ *
45
+ * Emits `awaitingWallet` immediately (before the await that hands control to the
46
+ * wallet), then exactly one terminal — `walletSucceeded` (success),
47
+ * `walletRejected` (explicit user no), or `walletFailed` (any other error) — all
48
+ * carrying the same `handoffId` and a `durationMs`. An intentional abort emits no
49
+ * terminal (the caller cancelled; it isn't a wallet outcome) — services check
50
+ * their signal AFTER this wrapper returns, so a wallet op that settled before
51
+ * the abort still records its truthful terminal first.
52
+ *
53
+ * A non-cancelling `walletTimedOut` marker fires if the deadline passes while the
54
+ * op is still pending; it is cleared the instant the op settles, so a fast op
55
+ * never false-fires it, and it NEVER rejects/aborts/mutates state — the wrapped
56
+ * promise stays the single terminal source of truth. This is a deliberate
57
+ * deviation from TON's cancelling `withWalletResponseTimeout`.
58
+ */
59
+ export async function instrumentHandoff(options, context, fn) {
60
+ const { eventManager, timeoutMs } = options;
61
+ const now = options.now ?? Date.now;
62
+ const handoffId = generateId();
63
+ const platform = derivePlatform();
64
+ const start = now();
65
+ // Flipped by the observational timeout timer; rides every terminal so a late
66
+ // completion is distinguishable from a fast one without a handoffId self-join.
67
+ let timedOut = false;
68
+ eventManager.emit('awaitingWallet', {
69
+ operation: context.operation,
70
+ connectionMode: context.connectionMode,
71
+ walletId: context.walletId,
72
+ namespace: context.namespace,
73
+ chainId: context.chainId,
74
+ handoffId,
75
+ walletFlowType: deriveWalletFlowType(context.connectionMode, platform),
76
+ platform,
77
+ ...(context.targetAlreadyActive !== undefined && {
78
+ targetAlreadyActive: context.targetAlreadyActive
79
+ })
80
+ });
81
+ // Observe page-visibility transitions ONLY while this handoff is pending — the
82
+ // app-switch signal for deep-link flows (left for the wallet / came back). The
83
+ // teardown in `finally` removes the listeners the instant the op settles.
84
+ const stopFocus = trackFocusDuringHandoff(eventManager, handoffId, platform);
85
+ // Same routing facets as `awaitingWallet` so every terminal is self-describing
86
+ // (no handoffId self-join needed to know which wallet / chain / mode it closed).
87
+ const routing = {
88
+ connectionMode: context.connectionMode,
89
+ walletId: context.walletId,
90
+ namespace: context.namespace,
91
+ chainId: context.chainId,
92
+ ...(context.targetAlreadyActive !== undefined && {
93
+ targetAlreadyActive: context.targetAlreadyActive
94
+ })
95
+ };
96
+ const shouldTime = timeoutMs > 0 && context.connectionMode !== 'tonConnect';
97
+ const timer = shouldTime
98
+ ? setTimeout(() => {
99
+ timedOut = true;
100
+ eventManager.emit('walletTimedOut', {
101
+ handoffId,
102
+ operation: context.operation,
103
+ durationMs: now() - start,
104
+ timeoutMs,
105
+ ...routing
106
+ });
107
+ }, timeoutMs)
108
+ : undefined;
109
+ try {
110
+ const result = await fn();
111
+ eventManager.emit('walletSucceeded', {
112
+ handoffId,
113
+ operation: context.operation,
114
+ durationMs: now() - start,
115
+ timedOut,
116
+ timeoutMs: timedOut ? timeoutMs : undefined,
117
+ ...routing
118
+ });
119
+ return result;
120
+ }
121
+ catch (error) {
122
+ // Caller-driven cancellation isn't a wallet outcome — leave it unterminated,
123
+ // mirroring the façade's `emitError` AbortError skip. Checked BEFORE rejection
124
+ // is safe: real user rejections surface as WalletConnectorError{rejected} or
125
+ // EIP-1193 4001 (see isUserRejection), never as an AbortError — within UWC's
126
+ // dependency surface AbortError is produced only by the caller's AbortSignal.
127
+ // The signal covers `abort(customReason)`, whose rejection value carries the
128
+ // caller's own name; a real wallet failure that lands while the caller has
129
+ // already aborted is intentionally folded into the cancellation — the caller
130
+ // walked away, so the outcome no longer describes a user-visible interaction.
131
+ if (isAbortError(error) || options.signal?.aborted)
132
+ throw error;
133
+ const durationMs = now() - start;
134
+ const { errorCode, failureKind, source } = classifyError(error);
135
+ // Either branch below emits a terminal for this error — record that so the
136
+ // façade's back-compat `error` event can mark itself as a duplicate.
137
+ markHandoffTerminal(error);
138
+ if (isUserRejection(error)) {
139
+ eventManager.emit('walletRejected', {
140
+ handoffId,
141
+ operation: context.operation,
142
+ durationMs,
143
+ timedOut,
144
+ timeoutMs: timedOut ? timeoutMs : undefined,
145
+ errorCode,
146
+ failureKind,
147
+ source,
148
+ ...routing
149
+ });
150
+ }
151
+ else {
152
+ eventManager.emit('walletFailed', {
153
+ handoffId,
154
+ operation: context.operation,
155
+ durationMs,
156
+ error: toWalletError(error),
157
+ timedOut,
158
+ timeoutMs: timedOut ? timeoutMs : undefined,
159
+ errorCode,
160
+ failureKind,
161
+ source,
162
+ ...routing
163
+ });
164
+ }
165
+ throw error;
166
+ }
167
+ finally {
168
+ if (timer)
169
+ clearTimeout(timer);
170
+ stopFocus();
171
+ }
172
+ }
173
+ //# sourceMappingURL=instrument-handoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instrument-handoff.js","sourceRoot":"","sources":["../../src/observability/instrument-handoff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ import type { WalletLifecycleEvent, ConnectionMode, Namespace, NetworkId } from '@meshconnect/uwc-types';
2
+ import type { EventManager } from '../managers/event-manager';
3
+ /** Routing facets stamped on every lifecycle event. No address. */
4
+ export interface LifecycleRouting {
5
+ connectionMode: ConnectionMode;
6
+ walletId?: string | undefined;
7
+ namespace?: Namespace | undefined;
8
+ }
9
+ /**
10
+ * Hard ceiling on lifecycle emissions per attachment. Every other telemetry
11
+ * sink this layer added is bounded (relay / bridge / observer-error dedup sets
12
+ * cap at 100); a provider looping `chainChanged`/`accountsChanged` must not be
13
+ * the one unbounded path to the observer. A legitimate session stays far below
14
+ * this; a storming provider goes silent instead of driving egress cost.
15
+ */
16
+ export declare const MAX_LIFECYCLE_EVENTS_PER_ATTACHMENT = 100;
17
+ export interface AttachLifecycleOptions {
18
+ eventManager: EventManager;
19
+ /**
20
+ * The active connector's `onLifecycleEvent` (already bound). `undefined` when the
21
+ * connector can't observe lifecycle at all; the value it returns is `undefined`
22
+ * when THIS session can't be observed (e.g. no live `.on`). Either way the
23
+ * bridge emits `lifecycleTelemetryUnavailable` and becomes a no-op.
24
+ */
25
+ subscribe: ((l: (e: WalletLifecycleEvent) => void) => (() => void) | undefined) | undefined;
26
+ /** Resolved per-event so a mid-session account/chain change is reflected. */
27
+ getRouting: () => LifecycleRouting;
28
+ connectionMode: ConnectionMode;
29
+ namespace?: Namespace | undefined;
30
+ walletId?: string | undefined;
31
+ /** Direct page vs production iframe — stamped on `lifecycleTelemetryUnavailable`. */
32
+ runtime?: 'direct' | 'iframe';
33
+ /**
34
+ * Window (ms) within which a wallet `chainChanged` is treated as the echo of a
35
+ * UWC-initiated `switchNetwork` and suppressed. Default 3000.
36
+ */
37
+ echoWindowMs?: number;
38
+ /**
39
+ * Seed for echo suppression when (re)attaching right AFTER a UWC-initiated
40
+ * switch completed (the cross-namespace reattach path): the fresh closure
41
+ * never saw that switch's `awaitingWallet`/`networkSwitched`, so the new
42
+ * provider's echo would otherwise surface as wallet-initiated. Same one-shot
43
+ * consume semantics as a bus-armed switch.
44
+ */
45
+ initialSwitch?: {
46
+ chainId: NetworkId;
47
+ at: number;
48
+ } | undefined;
49
+ now?: () => number;
50
+ }
51
+ /**
52
+ * Bridge a connector's PII-safe `WalletLifecycleEvent` stream onto the event bus
53
+ * as `walletAccountChanged` / `walletChainChanged` / `walletDisconnected` /
54
+ * `walletSessionExpired`.
55
+ *
56
+ * Two correctness concerns are handled here:
57
+ * - **Echo suppression.** When UWC drives a `switchNetwork`, the wallet emits its
58
+ * own `chainChanged` for the same chain — that's already captured by UWC's
59
+ * `networkSwitched` / the switch handoff, so re-emitting it as a wallet-
60
+ * initiated `walletChainChanged` would double-count. We watch the bus for
61
+ * `networkSwitched` and drop a matching `chainChanged` inside `echoWindowMs`.
62
+ * - **Availability is signal.** If the connector can't observe lifecycle, we emit
63
+ * `lifecycleTelemetryUnavailable` ONCE so a dashboard can tell "silent" from
64
+ * "unwired".
65
+ *
66
+ * @returns a teardown that removes the connector subscription AND the bus watcher.
67
+ */
68
+ export declare function attachLifecycle(opts: AttachLifecycleOptions): () => void;
69
+ //# sourceMappingURL=lifecycle-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle-bridge.d.ts","sourceRoot":"","sources":["../../src/observability/lifecycle-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,cAAc,EACd,SAAS,EACT,SAAS,EAEV,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE7D,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,cAAc,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,SAAS,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;CAClC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mCAAmC,MAAM,CAAA;AAEtD,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,YAAY,CAAA;IAC1B;;;;;OAKG;IACH,SAAS,EACL,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,oBAAoB,KAAK,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC,GACpE,SAAS,CAAA;IACb,6EAA6E;IAC7E,UAAU,EAAE,MAAM,gBAAgB,CAAA;IAClC,cAAc,EAAE,cAAc,CAAA;IAC9B,SAAS,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,qFAAqF;IACrF,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC7B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE;QAAE,OAAO,EAAE,SAAS,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAA;IAC9D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,MAAM,IAAI,CA2IxE"}