@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,54 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import type { Session, WalletMetadata } from '@meshconnect/uwc-types'
3
+ import { resolveSessionProvider } from './resolve-session-provider'
4
+
5
+ const extensionProvider = { kind: 'extension' }
6
+ const integratedProvider = { kind: 'integrated' }
7
+ const walletConnectProvider = { kind: 'walletConnect' }
8
+
9
+ const wallet = {
10
+ id: 'metamask',
11
+ name: 'MetaMask',
12
+ extensionInjectedProvider: extensionProvider,
13
+ integratedBrowserInjectedProvider: integratedProvider,
14
+ walletConnectProvider
15
+ } as unknown as WalletMetadata
16
+
17
+ const session = (
18
+ connectionMode: Session['connectionMode'],
19
+ activeWallet: WalletMetadata | null = wallet
20
+ ) => ({ connectionMode, activeWallet }) as Session
21
+
22
+ describe('resolveSessionProvider', () => {
23
+ it('resolves the extension provider for injected outside the integrated browser', () => {
24
+ expect(resolveSessionProvider(session('injected'), false)).toBe(
25
+ extensionProvider
26
+ )
27
+ })
28
+
29
+ it('resolves the integrated-browser provider for injected inside it', () => {
30
+ expect(resolveSessionProvider(session('injected'), true)).toBe(
31
+ integratedProvider
32
+ )
33
+ })
34
+
35
+ it('resolves the walletConnect provider for walletConnect', () => {
36
+ expect(resolveSessionProvider(session('walletConnect'), false)).toBe(
37
+ walletConnectProvider
38
+ )
39
+ })
40
+
41
+ it('yields undefined for tonConnect — the connector resolves its own provider', () => {
42
+ expect(resolveSessionProvider(session('tonConnect'), false)).toBeUndefined()
43
+ })
44
+
45
+ it('throws when the resolved provider is missing (configuration failure)', () => {
46
+ const bare = { id: 'metamask', name: 'MetaMask' } as WalletMetadata
47
+ expect(() =>
48
+ resolveSessionProvider(session('injected', bare), false)
49
+ ).toThrow('No provider available for the active wallet')
50
+ expect(() =>
51
+ resolveSessionProvider(session('walletConnect', bare), false)
52
+ ).toThrow('No provider available for the active wallet')
53
+ })
54
+ })
@@ -0,0 +1,25 @@
1
+ import type { Session, SupportedProvider } from '@meshconnect/uwc-types'
2
+
3
+ /**
4
+ * Resolve the active session's provider for a wallet-boundary op. This is
5
+ * validation, not wallet work — callers run it BEFORE `instrumentHandoff` so a
6
+ * missing provider (a configuration failure) never enters the handoff funnel.
7
+ * `tonConnect` resolves its provider inside the connector and legitimately
8
+ * yields undefined here.
9
+ */
10
+ export function resolveSessionProvider(
11
+ session: Session,
12
+ usingIntegratedBrowser: boolean
13
+ ): SupportedProvider | undefined {
14
+ if (session.connectionMode === 'tonConnect') return undefined
15
+ const provider =
16
+ session.connectionMode === 'injected'
17
+ ? usingIntegratedBrowser
18
+ ? session.activeWallet?.integratedBrowserInjectedProvider
19
+ : session.activeWallet?.extensionInjectedProvider
20
+ : session.activeWallet?.walletConnectProvider
21
+ if (!provider) {
22
+ throw new Error('No provider available for the active wallet')
23
+ }
24
+ return provider
25
+ }
@@ -0,0 +1,181 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ WalletConnectorError,
4
+ NoCommonNetworkError
5
+ } from '@meshconnect/uwc-types'
6
+ import {
7
+ classifyError,
8
+ isAbortError,
9
+ isUserRejection,
10
+ toWalletError
11
+ } from './to-wallet-error'
12
+
13
+ describe('classifyError', () => {
14
+ it('classifies an explicit user rejection (EIP-1193 4001)', () => {
15
+ expect(classifyError({ code: 4001, message: 'User rejected' })).toEqual({
16
+ errorCode: '4001',
17
+ failureKind: 'user_rejected',
18
+ source: 'provider'
19
+ })
20
+ })
21
+
22
+ it('classifies a WalletConnectorError{rejected} as user_rejected/connector', () => {
23
+ const err = new WalletConnectorError({ type: 'rejected', message: 'no' })
24
+ expect(classifyError(err)).toEqual({
25
+ failureKind: 'user_rejected',
26
+ source: 'connector'
27
+ })
28
+ })
29
+
30
+ it('classifies NoCommonNetworkError as no_common_network/connector', () => {
31
+ // Delta A: the #1 prod connection failure gets its own kind. It is a
32
+ // WalletConnectorError subclass, so source is 'connector'; it has no numeric
33
+ // code, so errorCode is omitted.
34
+ const err = new NoCommonNetworkError(['eip155:1'], ['solana:101'])
35
+ expect(classifyError(err)).toEqual({
36
+ failureKind: 'no_common_network',
37
+ source: 'connector'
38
+ })
39
+ })
40
+
41
+ it('classifies -32002 (request already pending) as provider_error', () => {
42
+ // Delta B: the double-fire race. A provider-level error, not a user reject.
43
+ expect(classifyError({ code: -32002, message: 'already pending' })).toEqual(
44
+ {
45
+ errorCode: '-32002',
46
+ failureKind: 'provider_error',
47
+ source: 'provider'
48
+ }
49
+ )
50
+ })
51
+
52
+ it('classifies an expired WalletConnectorError', () => {
53
+ const err = new WalletConnectorError({ type: 'expired', message: 'gone' })
54
+ expect(classifyError(err)).toEqual({
55
+ failureKind: 'expired',
56
+ source: 'connector'
57
+ })
58
+ })
59
+
60
+ it('maps known EIP-1193 codes to coarse kinds', () => {
61
+ expect(classifyError({ code: 4200 }).failureKind).toBe('unsupported_method')
62
+ expect(classifyError({ code: -32601 }).failureKind).toBe(
63
+ 'unsupported_method'
64
+ )
65
+ expect(classifyError({ code: -32602 }).failureKind).toBe('invalid_params')
66
+ expect(classifyError({ code: 4900 }).failureKind).toBe('network_error')
67
+ expect(classifyError({ code: 4901 }).failureKind).toBe('network_error')
68
+ })
69
+
70
+ it('falls back to unknown/unknown for a plain Error (no reliable signal)', () => {
71
+ // RELIABLE-ONLY: a plain Error carries no code and is not a typed connector
72
+ // error, so we never guess a kind from its message.
73
+ expect(classifyError(new Error('encoding overruns Uint8Array'))).toEqual({
74
+ failureKind: 'unknown',
75
+ source: 'unknown'
76
+ })
77
+ })
78
+
79
+ it('stringifies a string code without inventing a kind', () => {
80
+ expect(classifyError({ code: 'SERVER_ERROR' })).toEqual({
81
+ errorCode: 'SERVER_ERROR',
82
+ failureKind: 'unknown',
83
+ source: 'unknown'
84
+ })
85
+ })
86
+
87
+ it('scrubs a string error code (wallet-controlled free-form egress slot)', () => {
88
+ // A wallet can throw `{ code: '<anything>' }` — errorCode lands verbatim in
89
+ // walletRejected / walletFailed / error telemetry, so it must pass the same
90
+ // scrubber as error.message.
91
+ const addr = '0x' + 'a'.repeat(40)
92
+ const out = classifyError({ code: `denied for ${addr}` })
93
+ expect(out.errorCode).toBeDefined()
94
+ expect(out.errorCode).not.toContain(addr)
95
+ expect(out.errorCode).toContain('[redacted-hex]')
96
+ })
97
+
98
+ it('caps an over-long string error code', () => {
99
+ // '#' matches no scrub pattern, so this exercises the length cap itself.
100
+ const out = classifyError({ code: '#'.repeat(300) })
101
+ expect(out.errorCode).toBe('#'.repeat(64))
102
+ })
103
+
104
+ it('keeps known short string codes verbatim (ACTION_REJECTED)', () => {
105
+ expect(classifyError({ code: 'ACTION_REJECTED' })).toEqual({
106
+ errorCode: 'ACTION_REJECTED',
107
+ failureKind: 'user_rejected',
108
+ source: 'unknown'
109
+ })
110
+ })
111
+ })
112
+
113
+ describe('isUserRejection', () => {
114
+ it('is true for 4001 and ACTION_REJECTED and WalletConnectorError{rejected}', () => {
115
+ expect(isUserRejection({ code: 4001 })).toBe(true)
116
+ expect(isUserRejection({ code: 'ACTION_REJECTED' })).toBe(true)
117
+ expect(
118
+ isUserRejection(
119
+ new WalletConnectorError({ type: 'rejected', message: '' })
120
+ )
121
+ ).toBe(true)
122
+ })
123
+
124
+ it('is false for a NoCommonNetworkError (not a user action)', () => {
125
+ expect(isUserRejection(new NoCommonNetworkError([], []))).toBe(false)
126
+ })
127
+ })
128
+
129
+ describe('toWalletError', () => {
130
+ it('preserves the type of a NoCommonNetworkError (unknown)', () => {
131
+ const err = new NoCommonNetworkError(['eip155:1'], ['solana:101'])
132
+ expect(toWalletError(err)).toEqual({
133
+ type: 'unknown',
134
+ message:
135
+ 'No common supported network found between wallet and configured networks'
136
+ })
137
+ })
138
+
139
+ it('bounds an over-long non-connector message (shared safeErrorMessage cap)', () => {
140
+ const err = new Error('x'.repeat(10_000))
141
+ expect(toWalletError(err).message).toHaveLength(500)
142
+ })
143
+ })
144
+
145
+ describe('poisoned error objects (throwing getters)', () => {
146
+ // These helpers run inside telemetry catch blocks — a getter that throws
147
+ // must never replace the real rejection with its own error.
148
+ const poisoned = () => {
149
+ const evil = new Error('real message')
150
+ for (const prop of ['message', 'code', 'name'] as const) {
151
+ Object.defineProperty(evil, prop, {
152
+ get() {
153
+ throw new Error(`poisoned ${prop}`)
154
+ }
155
+ })
156
+ }
157
+ return evil
158
+ }
159
+
160
+ it('toWalletError falls back instead of throwing', () => {
161
+ expect(toWalletError(poisoned())).toEqual({
162
+ type: 'unknown',
163
+ message: 'Unreadable error object'
164
+ })
165
+ })
166
+
167
+ it('isUserRejection returns false instead of throwing', () => {
168
+ expect(isUserRejection(poisoned())).toBe(false)
169
+ })
170
+
171
+ it('classifyError returns unknown/unknown instead of throwing', () => {
172
+ expect(classifyError(poisoned())).toEqual({
173
+ failureKind: 'unknown',
174
+ source: 'unknown'
175
+ })
176
+ })
177
+
178
+ it('isAbortError returns false instead of throwing', () => {
179
+ expect(isAbortError(poisoned())).toBe(false)
180
+ })
181
+ })
@@ -0,0 +1,152 @@
1
+ import {
2
+ WalletConnectorError,
3
+ NoCommonNetworkError,
4
+ safeErrorMessage,
5
+ type WalletError,
6
+ type FailureKind,
7
+ type ErrorSource
8
+ } from '@meshconnect/uwc-types'
9
+ import { scrubMessage } from '../observability/scrub'
10
+
11
+ /**
12
+ * Cap for STRING error codes. A wallet controls `error.code`, so an unbounded
13
+ * string here is a free-form slot on the telemetry egress path. The longest
14
+ * legitimate enum-like code in the wild (ethers') is ~23 chars — 64 keeps every
15
+ * real code intact while bounding abuse.
16
+ */
17
+ const MAX_ERROR_CODE_LENGTH = 64
18
+
19
+ /**
20
+ * Normalize any thrown value into the PII-light `WalletError` shape.
21
+ *
22
+ * Every helper in this file guards its property reads: they run inside the
23
+ * telemetry catch blocks, where a poisoned getter (`code`/`message`/`toString`
24
+ * that throws) would otherwise REPLACE the real rejection with its own error.
25
+ */
26
+ export function toWalletError(error: unknown): WalletError {
27
+ try {
28
+ if (error instanceof WalletConnectorError) {
29
+ return { type: error.type, message: error.message }
30
+ }
31
+ return { type: 'unknown', message: safeErrorMessage(error) }
32
+ } catch {
33
+ return { type: 'unknown', message: 'Unreadable error object' }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Whether a thrown value is an explicit user rejection. Connectors usually wrap
39
+ * these as `WalletConnectorError{type:'rejected'}`, but a raw EIP-1193 reject
40
+ * (code `4001`) or ethers' `ACTION_REJECTED` can also reach the façade.
41
+ */
42
+ export function isUserRejection(error: unknown): boolean {
43
+ try {
44
+ if (error instanceof WalletConnectorError) return error.type === 'rejected'
45
+ const code = (error as { code?: unknown } | null | undefined)?.code
46
+ return code === 4001 || code === 'ACTION_REJECTED'
47
+ } catch {
48
+ return false
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Classify a thrown wallet error into stable telemetry facets. RELIABLE-ONLY:
54
+ * every branch keys off a structural signal (a typed `WalletConnectorError`, an
55
+ * explicit user rejection, or a known EIP-1193 numeric code). Anything we can't
56
+ * place from that evidence stays `unknown` — we never infer a kind/source from a
57
+ * free-form message, which would be a guess and would also risk leaking content
58
+ * into a structured field.
59
+ */
60
+ export function classifyError(error: unknown): {
61
+ errorCode?: string
62
+ failureKind: FailureKind
63
+ source: ErrorSource
64
+ } {
65
+ try {
66
+ return classifyErrorUnguarded(error)
67
+ } catch {
68
+ return { failureKind: 'unknown', source: 'unknown' }
69
+ }
70
+ }
71
+
72
+ function classifyErrorUnguarded(error: unknown): {
73
+ errorCode?: string
74
+ failureKind: FailureKind
75
+ source: ErrorSource
76
+ } {
77
+ const code = (error as { code?: unknown } | null | undefined)?.code
78
+ const hasNumericCode = typeof code === 'number'
79
+
80
+ // Numeric codes pass through; STRING codes are wallet-controlled free-form
81
+ // data headed for structured telemetry fields, so they get the same scrubber
82
+ // as error.message plus a length cap. Known enum-likes ('ACTION_REJECTED')
83
+ // are short and pattern-free — they pass unchanged.
84
+ const errorCode =
85
+ typeof code === 'number'
86
+ ? String(code)
87
+ : typeof code === 'string'
88
+ ? scrubMessage(code).slice(0, MAX_ERROR_CODE_LENGTH)
89
+ : undefined
90
+
91
+ const source: ErrorSource =
92
+ error instanceof WalletConnectorError
93
+ ? 'connector'
94
+ : hasNumericCode
95
+ ? 'provider'
96
+ : 'unknown'
97
+
98
+ let failureKind: FailureKind = 'unknown'
99
+ if (isUserRejection(error)) {
100
+ failureKind = 'user_rejected'
101
+ } else if (error instanceof NoCommonNetworkError) {
102
+ // The wallet and configured networks don't overlap — the #1 prod connection
103
+ // failure. Carries its own CAIP-2 id lists; the caller forwards them.
104
+ failureKind = 'no_common_network'
105
+ } else if (
106
+ error instanceof WalletConnectorError &&
107
+ error.type === 'expired'
108
+ ) {
109
+ failureKind = 'expired'
110
+ } else if (hasNumericCode) {
111
+ switch (code) {
112
+ case 4200:
113
+ case -32601:
114
+ failureKind = 'unsupported_method'
115
+ break
116
+ case -32602:
117
+ failureKind = 'invalid_params'
118
+ break
119
+ case 4900:
120
+ case 4901:
121
+ failureKind = 'network_error'
122
+ break
123
+ // -32002 = "resource unavailable": a connect/permission request is already
124
+ // pending for this origin. The double-fire race seen in prod (a second
125
+ // connect while one is open) — a provider-level error, not a user reject.
126
+ case -32002:
127
+ failureKind = 'provider_error'
128
+ break
129
+ }
130
+ }
131
+
132
+ return errorCode === undefined
133
+ ? { failureKind, source }
134
+ : { errorCode, failureKind, source }
135
+ }
136
+
137
+ /**
138
+ * Whether a thrown value is an intentional cancellation (AbortSignal / fetch
139
+ * abort). Matched by `name` rather than `instanceof Error` because a real abort is
140
+ * a `DOMException`, which is not an `Error` subclass in every runtime.
141
+ */
142
+ export function isAbortError(error: unknown): boolean {
143
+ try {
144
+ return (
145
+ typeof error === 'object' &&
146
+ error !== null &&
147
+ (error as { name?: unknown }).name === 'AbortError'
148
+ )
149
+ } catch {
150
+ return false
151
+ }
152
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import type {
3
+ ConnectionMode,
4
+ Connector,
5
+ Session,
6
+ WalletMetadata
7
+ } from '@meshconnect/uwc-types'
8
+ import { SessionManager } from '../managers/session-manager'
9
+ import {
10
+ validateWalletSession,
11
+ getSessionConnector
12
+ } from './validate-wallet-session'
13
+
14
+ const wallet = { id: 'metamask', name: 'MetaMask' } as WalletMetadata
15
+
16
+ const managerWith = (session: Partial<Session>) => {
17
+ const manager = new SessionManager()
18
+ manager.updateSession(session)
19
+ return manager
20
+ }
21
+
22
+ describe('validateWalletSession', () => {
23
+ it('returns the session when all guards pass', () => {
24
+ const manager = managerWith({
25
+ connectionMode: 'injected',
26
+ activeWallet: wallet,
27
+ activeAddress: '0x123'
28
+ })
29
+ const session = validateWalletSession(manager, { requireAddress: true })
30
+ expect(session.activeWallet).toBe(wallet)
31
+ })
32
+
33
+ it('throws the public-API messages in guard order', () => {
34
+ expect(() =>
35
+ validateWalletSession(new SessionManager(), { requireAddress: true })
36
+ ).toThrow('No active wallet')
37
+ expect(() =>
38
+ validateWalletSession(managerWith({ activeWallet: wallet }), {
39
+ requireAddress: true
40
+ })
41
+ ).toThrow('No active connection')
42
+ expect(() =>
43
+ validateWalletSession(
44
+ managerWith({ activeWallet: wallet, connectionMode: 'injected' }),
45
+ { requireAddress: true }
46
+ )
47
+ ).toThrow('No active wallet address')
48
+ })
49
+
50
+ it('skips the address guard when requireAddress is false (Solana bytes path)', () => {
51
+ const manager = managerWith({
52
+ connectionMode: 'injected',
53
+ activeWallet: wallet
54
+ })
55
+ expect(
56
+ validateWalletSession(manager, { requireAddress: false }).connectionMode
57
+ ).toBe('injected')
58
+ })
59
+ })
60
+
61
+ describe('getSessionConnector', () => {
62
+ const session = { connectionMode: 'injected' } as Session
63
+
64
+ it('returns the connector registered for the session mode', () => {
65
+ const connector = { connect: vi.fn() } as unknown as Connector
66
+ const connectors = new Map<ConnectionMode, Connector>([
67
+ ['injected', connector]
68
+ ])
69
+ expect(getSessionConnector(connectors, session)).toBe(connector)
70
+ })
71
+
72
+ it('throws the public-API message when no connector is registered', () => {
73
+ expect(() => getSessionConnector(new Map(), session)).toThrow(
74
+ 'No connector found for connection mode: injected'
75
+ )
76
+ })
77
+ })
@@ -0,0 +1,40 @@
1
+ import type { ConnectionMode, Connector, Session } from '@meshconnect/uwc-types'
2
+ import type { SessionManager } from '../managers/session-manager'
3
+
4
+ /**
5
+ * Session guards shared by the signature/transaction services — the thrown
6
+ * messages are part of the public API and must stay stable. Runs BEFORE
7
+ * `instrumentHandoff` so a pre-wallet failure never enters the handoff funnel.
8
+ * The Solana bytes path deliberately skips the address guard
9
+ * (`requireAddress: false`) — it signs caller-supplied bytes.
10
+ */
11
+ export function validateWalletSession(
12
+ sessionManager: SessionManager,
13
+ options: { requireAddress: boolean }
14
+ ): Session {
15
+ const session = sessionManager.getSession()
16
+ if (!session.activeWallet) {
17
+ throw new Error('No active wallet')
18
+ }
19
+ if (!session.connectionMode) {
20
+ throw new Error('No active connection')
21
+ }
22
+ if (options.requireAddress && !session.activeAddress) {
23
+ throw new Error('No active wallet address')
24
+ }
25
+ return session
26
+ }
27
+
28
+ /** Resolve the active session's connector — validation, same funnel rule. */
29
+ export function getSessionConnector(
30
+ connectors: Map<ConnectionMode, Connector>,
31
+ session: Session
32
+ ): Connector {
33
+ const connector = connectors.get(session.connectionMode!)
34
+ if (!connector) {
35
+ throw new Error(
36
+ `No connector found for connection mode: ${session.connectionMode}`
37
+ )
38
+ }
39
+ return connector
40
+ }
@@ -0,0 +1,11 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { UWC_CORE_VERSION } from './version'
3
+ import pkg from '../package.json'
4
+
5
+ describe('UWC_CORE_VERSION', () => {
6
+ it('matches package.json (stale? run: node scripts/generate-version.mjs)', () => {
7
+ // The constant is generated from package.json at build / version-packages
8
+ // time; this guard catches a manual bump that skipped regeneration.
9
+ expect(UWC_CORE_VERSION).toBe(pkg.version)
10
+ })
11
+ })
package/src/version.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Generated from package.json by scripts/generate-version.mjs — do not edit.
2
+ export const UWC_CORE_VERSION = '1.1.0'