@cmdoss/memwal-sdk 0.6.2 → 0.8.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 (247) hide show
  1. package/ARCHITECTURE.md +547 -547
  2. package/BENCHMARKS.md +238 -238
  3. package/README.md +310 -181
  4. package/dist/ai-sdk/tools.d.ts +2 -2
  5. package/dist/ai-sdk/tools.js +2 -2
  6. package/dist/client/ClientMemoryManager.js +2 -2
  7. package/dist/client/ClientMemoryManager.js.map +1 -1
  8. package/dist/client/PersonalDataWallet.d.ts.map +1 -1
  9. package/dist/client/SimplePDWClient.d.ts +29 -1
  10. package/dist/client/SimplePDWClient.d.ts.map +1 -1
  11. package/dist/client/SimplePDWClient.js +45 -13
  12. package/dist/client/SimplePDWClient.js.map +1 -1
  13. package/dist/client/namespaces/EmbeddingsNamespace.d.ts +1 -1
  14. package/dist/client/namespaces/EmbeddingsNamespace.js +1 -1
  15. package/dist/client/namespaces/MemoryNamespace.d.ts +31 -0
  16. package/dist/client/namespaces/MemoryNamespace.d.ts.map +1 -1
  17. package/dist/client/namespaces/MemoryNamespace.js +272 -39
  18. package/dist/client/namespaces/MemoryNamespace.js.map +1 -1
  19. package/dist/client/namespaces/consolidated/AINamespace.d.ts +2 -2
  20. package/dist/client/namespaces/consolidated/AINamespace.js +2 -2
  21. package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts +12 -2
  22. package/dist/client/namespaces/consolidated/BlockchainNamespace.d.ts.map +1 -1
  23. package/dist/client/namespaces/consolidated/BlockchainNamespace.js +62 -4
  24. package/dist/client/namespaces/consolidated/BlockchainNamespace.js.map +1 -1
  25. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts +67 -2
  26. package/dist/client/namespaces/consolidated/StorageNamespace.d.ts.map +1 -1
  27. package/dist/client/namespaces/consolidated/StorageNamespace.js +549 -16
  28. package/dist/client/namespaces/consolidated/StorageNamespace.js.map +1 -1
  29. package/dist/config/ConfigurationHelper.js +61 -61
  30. package/dist/config/defaults.js +2 -2
  31. package/dist/config/defaults.js.map +1 -1
  32. package/dist/graph/GraphService.js +21 -21
  33. package/dist/graph/GraphService.js.map +1 -1
  34. package/dist/index.d.ts +3 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +3 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/infrastructure/seal/EncryptionService.d.ts +9 -5
  39. package/dist/infrastructure/seal/EncryptionService.d.ts.map +1 -1
  40. package/dist/infrastructure/seal/EncryptionService.js +37 -15
  41. package/dist/infrastructure/seal/EncryptionService.js.map +1 -1
  42. package/dist/infrastructure/seal/SealService.d.ts +13 -5
  43. package/dist/infrastructure/seal/SealService.d.ts.map +1 -1
  44. package/dist/infrastructure/seal/SealService.js +36 -34
  45. package/dist/infrastructure/seal/SealService.js.map +1 -1
  46. package/dist/langchain/createPDWRAG.js +30 -30
  47. package/dist/retrieval/MemoryDecryptionPipeline.d.ts.map +1 -1
  48. package/dist/retrieval/MemoryDecryptionPipeline.js +2 -1
  49. package/dist/retrieval/MemoryDecryptionPipeline.js.map +1 -1
  50. package/dist/retrieval/MemoryRetrievalService.d.ts +31 -0
  51. package/dist/retrieval/MemoryRetrievalService.d.ts.map +1 -1
  52. package/dist/retrieval/MemoryRetrievalService.js +44 -4
  53. package/dist/retrieval/MemoryRetrievalService.js.map +1 -1
  54. package/dist/services/CapabilityService.d.ts.map +1 -1
  55. package/dist/services/CapabilityService.js +30 -14
  56. package/dist/services/CapabilityService.js.map +1 -1
  57. package/dist/services/CrossContextPermissionService.d.ts.map +1 -1
  58. package/dist/services/CrossContextPermissionService.js +9 -7
  59. package/dist/services/CrossContextPermissionService.js.map +1 -1
  60. package/dist/services/EmbeddingService.d.ts +28 -1
  61. package/dist/services/EmbeddingService.d.ts.map +1 -1
  62. package/dist/services/EmbeddingService.js +54 -0
  63. package/dist/services/EmbeddingService.js.map +1 -1
  64. package/dist/services/EncryptionService.d.ts.map +1 -1
  65. package/dist/services/EncryptionService.js +6 -5
  66. package/dist/services/EncryptionService.js.map +1 -1
  67. package/dist/services/GeminiAIService.js +309 -309
  68. package/dist/services/IndexManager.d.ts +5 -1
  69. package/dist/services/IndexManager.d.ts.map +1 -1
  70. package/dist/services/IndexManager.js +17 -40
  71. package/dist/services/IndexManager.js.map +1 -1
  72. package/dist/services/QueryService.js +1 -1
  73. package/dist/services/QueryService.js.map +1 -1
  74. package/dist/services/StorageService.d.ts +11 -0
  75. package/dist/services/StorageService.d.ts.map +1 -1
  76. package/dist/services/StorageService.js +73 -10
  77. package/dist/services/StorageService.js.map +1 -1
  78. package/dist/services/TransactionService.d.ts +20 -0
  79. package/dist/services/TransactionService.d.ts.map +1 -1
  80. package/dist/services/TransactionService.js +43 -0
  81. package/dist/services/TransactionService.js.map +1 -1
  82. package/dist/services/ViewService.js +2 -2
  83. package/dist/services/ViewService.js.map +1 -1
  84. package/dist/services/storage/QuiltBatchManager.d.ts +101 -1
  85. package/dist/services/storage/QuiltBatchManager.d.ts.map +1 -1
  86. package/dist/services/storage/QuiltBatchManager.js +410 -20
  87. package/dist/services/storage/QuiltBatchManager.js.map +1 -1
  88. package/dist/services/storage/index.d.ts +1 -1
  89. package/dist/services/storage/index.d.ts.map +1 -1
  90. package/dist/services/storage/index.js.map +1 -1
  91. package/dist/utils/LRUCache.d.ts +106 -0
  92. package/dist/utils/LRUCache.d.ts.map +1 -0
  93. package/dist/utils/LRUCache.js +281 -0
  94. package/dist/utils/LRUCache.js.map +1 -0
  95. package/dist/utils/index.d.ts +1 -0
  96. package/dist/utils/index.d.ts.map +1 -1
  97. package/dist/utils/index.js +2 -0
  98. package/dist/utils/index.js.map +1 -1
  99. package/dist/utils/memoryIndexOnChain.d.ts +212 -0
  100. package/dist/utils/memoryIndexOnChain.d.ts.map +1 -0
  101. package/dist/utils/memoryIndexOnChain.js +312 -0
  102. package/dist/utils/memoryIndexOnChain.js.map +1 -0
  103. package/dist/utils/rebuildIndexNode.d.ts +29 -0
  104. package/dist/utils/rebuildIndexNode.d.ts.map +1 -1
  105. package/dist/utils/rebuildIndexNode.js +366 -98
  106. package/dist/utils/rebuildIndexNode.js.map +1 -1
  107. package/dist/vector/HnswWasmService.d.ts +20 -5
  108. package/dist/vector/HnswWasmService.d.ts.map +1 -1
  109. package/dist/vector/HnswWasmService.js +73 -40
  110. package/dist/vector/HnswWasmService.js.map +1 -1
  111. package/dist/vector/IHnswService.d.ts +10 -1
  112. package/dist/vector/IHnswService.d.ts.map +1 -1
  113. package/dist/vector/IHnswService.js.map +1 -1
  114. package/dist/vector/NodeHnswService.d.ts +16 -0
  115. package/dist/vector/NodeHnswService.d.ts.map +1 -1
  116. package/dist/vector/NodeHnswService.js +84 -5
  117. package/dist/vector/NodeHnswService.js.map +1 -1
  118. package/dist/vector/createHnswService.d.ts +1 -1
  119. package/dist/vector/createHnswService.js +1 -1
  120. package/dist/vector/index.d.ts +1 -1
  121. package/dist/vector/index.js +1 -1
  122. package/package.json +157 -157
  123. package/src/access/PermissionService.ts +635 -635
  124. package/src/aggregation/AggregationService.ts +389 -389
  125. package/src/ai-sdk/PDWVectorStore.ts +715 -715
  126. package/src/ai-sdk/index.ts +65 -65
  127. package/src/ai-sdk/tools.ts +460 -460
  128. package/src/ai-sdk/types.ts +404 -404
  129. package/src/batch/BatchManager.ts +597 -597
  130. package/src/batch/BatchingService.ts +429 -429
  131. package/src/batch/MemoryProcessingCache.ts +492 -492
  132. package/src/batch/index.ts +30 -30
  133. package/src/browser.ts +200 -200
  134. package/src/client/ClientMemoryManager.ts +987 -987
  135. package/src/client/PersonalDataWallet.ts +345 -345
  136. package/src/client/SimplePDWClient.ts +1289 -1222
  137. package/src/client/factory.ts +154 -154
  138. package/src/client/namespaces/AnalyticsNamespace.ts +377 -377
  139. package/src/client/namespaces/BatchNamespace.ts +356 -356
  140. package/src/client/namespaces/CacheNamespace.ts +123 -123
  141. package/src/client/namespaces/CapabilityNamespace.ts +217 -217
  142. package/src/client/namespaces/ClassifyNamespace.ts +169 -169
  143. package/src/client/namespaces/ContextNamespace.ts +297 -297
  144. package/src/client/namespaces/EmbeddingsNamespace.ts +99 -99
  145. package/src/client/namespaces/EncryptionNamespace.ts +221 -221
  146. package/src/client/namespaces/GraphNamespace.ts +468 -468
  147. package/src/client/namespaces/IndexNamespace.ts +361 -361
  148. package/src/client/namespaces/MemoryNamespace.ts +1422 -1135
  149. package/src/client/namespaces/PermissionsNamespace.ts +254 -254
  150. package/src/client/namespaces/PipelineNamespace.ts +220 -220
  151. package/src/client/namespaces/SearchNamespace.ts +1049 -1049
  152. package/src/client/namespaces/StorageNamespace.ts +458 -458
  153. package/src/client/namespaces/TxNamespace.ts +260 -260
  154. package/src/client/namespaces/WalletNamespace.ts +243 -243
  155. package/src/client/namespaces/consolidated/AINamespace.ts +449 -449
  156. package/src/client/namespaces/consolidated/BlockchainNamespace.ts +607 -546
  157. package/src/client/namespaces/consolidated/SecurityNamespace.ts +648 -648
  158. package/src/client/namespaces/consolidated/StorageNamespace.ts +1141 -497
  159. package/src/client/namespaces/consolidated/index.ts +39 -39
  160. package/src/client/signers/KeypairSigner.ts +108 -108
  161. package/src/client/signers/UnifiedSigner.ts +110 -110
  162. package/src/client/signers/WalletAdapterSigner.ts +159 -159
  163. package/src/client/signers/index.ts +26 -26
  164. package/src/config/ConfigurationHelper.ts +412 -412
  165. package/src/config/defaults.ts +51 -51
  166. package/src/config/index.ts +8 -8
  167. package/src/config/validation.ts +70 -70
  168. package/src/core/index.ts +14 -14
  169. package/src/core/interfaces/IService.ts +307 -307
  170. package/src/core/interfaces/index.ts +8 -8
  171. package/src/core/types/capability.ts +297 -297
  172. package/src/core/types/index.ts +870 -870
  173. package/src/core/types/wallet.ts +270 -270
  174. package/src/core/types.ts +9 -9
  175. package/src/core/wallet.ts +222 -222
  176. package/src/embedding/index.ts +19 -19
  177. package/src/embedding/types.ts +357 -357
  178. package/src/errors/index.ts +602 -602
  179. package/src/errors/recovery.ts +461 -461
  180. package/src/errors/validation.ts +567 -567
  181. package/src/generated/pdw/capability.ts +319 -319
  182. package/src/graph/GraphService.ts +887 -887
  183. package/src/graph/KnowledgeGraphManager.ts +728 -728
  184. package/src/graph/index.ts +25 -25
  185. package/src/index.ts +498 -474
  186. package/src/infrastructure/index.ts +22 -22
  187. package/src/infrastructure/seal/EncryptionService.ts +628 -603
  188. package/src/infrastructure/seal/SealService.ts +613 -615
  189. package/src/infrastructure/seal/index.ts +9 -9
  190. package/src/infrastructure/sui/BlockchainManager.ts +627 -627
  191. package/src/infrastructure/sui/SuiService.ts +888 -888
  192. package/src/infrastructure/sui/index.ts +9 -9
  193. package/src/infrastructure/walrus/StorageManager.ts +604 -604
  194. package/src/infrastructure/walrus/WalrusStorageService.ts +612 -612
  195. package/src/infrastructure/walrus/index.ts +9 -9
  196. package/src/langchain/PDWEmbeddings.ts +145 -145
  197. package/src/langchain/PDWVectorStore.ts +456 -456
  198. package/src/langchain/createPDWRAG.ts +303 -303
  199. package/src/langchain/index.ts +47 -47
  200. package/src/permissions/ConsentRepository.browser.ts +249 -249
  201. package/src/permissions/ConsentRepository.ts +364 -364
  202. package/src/pipeline/MemoryPipeline.ts +862 -862
  203. package/src/pipeline/PipelineManager.ts +683 -683
  204. package/src/pipeline/index.ts +26 -26
  205. package/src/retrieval/AdvancedSearchService.ts +629 -629
  206. package/src/retrieval/MemoryAnalyticsService.ts +711 -711
  207. package/src/retrieval/MemoryDecryptionPipeline.ts +825 -824
  208. package/src/retrieval/MemoryRetrievalService.ts +904 -830
  209. package/src/retrieval/index.ts +42 -42
  210. package/src/services/BatchService.ts +352 -352
  211. package/src/services/CapabilityService.ts +464 -448
  212. package/src/services/ClassifierService.ts +465 -465
  213. package/src/services/CrossContextPermissionService.ts +486 -484
  214. package/src/services/EmbeddingService.ts +771 -706
  215. package/src/services/EncryptionService.ts +712 -711
  216. package/src/services/GeminiAIService.ts +753 -753
  217. package/src/services/IndexManager.ts +977 -1004
  218. package/src/services/MemoryIndexService.ts +1003 -1003
  219. package/src/services/MemoryService.ts +369 -369
  220. package/src/services/QueryService.ts +890 -890
  221. package/src/services/StorageService.ts +1182 -1111
  222. package/src/services/TransactionService.ts +838 -790
  223. package/src/services/VectorService.ts +462 -462
  224. package/src/services/ViewService.ts +484 -484
  225. package/src/services/index.ts +25 -25
  226. package/src/services/storage/BlobAttributesManager.ts +333 -333
  227. package/src/services/storage/KnowledgeGraphManager.ts +425 -425
  228. package/src/services/storage/MemorySearchManager.ts +387 -387
  229. package/src/services/storage/QuiltBatchManager.ts +1130 -660
  230. package/src/services/storage/WalrusMetadataManager.ts +268 -268
  231. package/src/services/storage/WalrusStorageManager.ts +287 -287
  232. package/src/services/storage/index.ts +57 -52
  233. package/src/types/index.ts +13 -13
  234. package/src/utils/LRUCache.ts +378 -0
  235. package/src/utils/index.ts +76 -68
  236. package/src/utils/memoryIndexOnChain.ts +507 -0
  237. package/src/utils/rebuildIndex.ts +290 -290
  238. package/src/utils/rebuildIndexNode.ts +771 -424
  239. package/src/vector/BrowserHnswIndexService.ts +758 -758
  240. package/src/vector/HnswWasmService.ts +731 -679
  241. package/src/vector/IHnswService.ts +233 -224
  242. package/src/vector/NodeHnswService.ts +833 -735
  243. package/src/vector/VectorManager.ts +478 -478
  244. package/src/vector/createHnswService.ts +135 -135
  245. package/src/vector/index.ts +56 -56
  246. package/src/wallet/ContextWalletService.ts +656 -656
  247. package/src/wallet/MainWalletService.ts +317 -317
@@ -1,636 +1,636 @@
1
- /**
2
- * PermissionService - OAuth-style access control management
3
- *
4
- * Manages permissions for cross-app data access, including:
5
- * - OAuth-style consent requests and grants
6
- * - Permission validation and enforcement
7
- * - On-chain access control integration
8
- * - Permission auditing and revocation
9
- */
10
-
11
- // Use Web Crypto API (browser-compatible)
12
- const randomUUID = (): string => crypto.randomUUID();
13
- import { SuiClient } from '@mysten/sui/client';
14
- import { Transaction } from '@mysten/sui/transactions';
15
- import { normalizeSuiAddress } from '@mysten/sui/utils';
16
- import type { Signer } from '@mysten/sui/cryptography';
17
-
18
- import {
19
- ConsentRequest,
20
- ConsentRequestRecord,
21
- ConsentStatus,
22
- AccessGrant,
23
- PermissionScope,
24
- RequestConsentOptions,
25
- GrantPermissionsOptions,
26
- RevokePermissionsOptions
27
- } from '../core/types/wallet.js';
28
- import { ContextWalletService } from '../wallet/ContextWalletService.js';
29
- import { CapabilityService } from '../services/CapabilityService.js';
30
- import { CrossContextPermissionService } from '../services/CrossContextPermissionService';
31
- import type { WalletAllowlistPermission } from '../services/CrossContextPermissionService';
32
- import type { ConsentRepository } from '../permissions/ConsentRepository.js';
33
-
34
- /**
35
- * Configuration for PermissionService
36
- */
37
- export interface PermissionServiceConfig {
38
- /** Sui client instance */
39
- suiClient: SuiClient;
40
- /** Package ID for Move contracts */
41
- packageId: string;
42
- /** Access registry ID for wallet allowlists */
43
- accessRegistryId: string;
44
- /**
45
- * @deprecated Use capabilityService instead for capability-based architecture
46
- * ContextWalletService for validation (legacy)
47
- */
48
- contextWalletService?: ContextWalletService;
49
- /** CapabilityService for capability-based validation (preferred) */
50
- capabilityService?: CapabilityService;
51
- /** Optional injected cross-context permission service */
52
- crossContextPermissionService?: CrossContextPermissionService;
53
- /** Optional repository for consent persistence */
54
- consentRepository?: ConsentRepository;
55
- }
56
-
57
- /**
58
- * PermissionService handles OAuth-style access control
59
- */
60
- export class PermissionService {
61
- private suiClient: SuiClient;
62
- private packageId: string;
63
- /** @deprecated Use capabilityService instead */
64
- private contextWalletService?: ContextWalletService;
65
- private capabilityService?: CapabilityService;
66
- private crossContextPermissions: CrossContextPermissionService;
67
- private pendingConsents: Map<string, ConsentRequestRecord> = new Map();
68
- private consentRepository?: ConsentRepository;
69
-
70
- constructor(config: PermissionServiceConfig) {
71
- this.suiClient = config.suiClient;
72
- this.packageId = config.packageId;
73
- this.contextWalletService = config.contextWalletService;
74
- this.capabilityService = config.capabilityService;
75
- this.consentRepository = config.consentRepository;
76
-
77
- if (!config.accessRegistryId) {
78
- throw new Error('PermissionService requires accessRegistryId for wallet-based permissions');
79
- }
80
-
81
- this.crossContextPermissions =
82
- config.crossContextPermissionService ??
83
- new CrossContextPermissionService(
84
- {
85
- packageId: this.packageId,
86
- accessRegistryId: config.accessRegistryId,
87
- },
88
- this.suiClient,
89
- );
90
- }
91
-
92
- /**
93
- * Request user consent for accessing data
94
- * @param options - Consent request options
95
- * @returns Created consent request
96
- */
97
- async requestConsent(options: RequestConsentOptions): Promise<ConsentRequestRecord> {
98
- const requesterWallet = normalizeSuiAddress(options.requesterWallet);
99
- const targetWallet = normalizeSuiAddress(options.targetWallet);
100
- const now = Date.now();
101
-
102
- const requestId = randomUUID();
103
- const consentRequest: ConsentRequestRecord = {
104
- requesterWallet,
105
- targetWallet,
106
- targetScopes: options.scopes,
107
- purpose: options.purpose,
108
- expiresAt: options.expiresIn ? now + options.expiresIn : undefined,
109
- requestId,
110
- createdAt: now,
111
- updatedAt: now,
112
- status: 'pending',
113
- };
114
-
115
- await this.persistConsentRequest(consentRequest);
116
-
117
- // TODO: Send consent request to backend for UI presentation
118
- // For now, return the persisted record so callers can track status locally
119
- return consentRequest;
120
- }
121
-
122
- /**
123
- * Grant permissions to an app (user approval)
124
- * @param userAddress - User granting permissions
125
- * @param options - Grant options
126
- * @returns Created access grant
127
- */
128
- async grantPermissions(
129
- userAddress: string,
130
- options: GrantPermissionsOptions & { signer?: Signer; appId?: string }
131
- ): Promise<AccessGrant> {
132
- const requestingWallet = normalizeSuiAddress(options.requestingWallet);
133
- const targetWallet = normalizeSuiAddress(options.targetWallet);
134
-
135
- // Validate ownership - prefer capability-based validation
136
- const hasAccess = await this.validateOwnership(userAddress, targetWallet, options.appId);
137
- if (!hasAccess) {
138
- throw new Error('User does not own the specified target wallet or capability');
139
- }
140
-
141
- let lastDigest: string | undefined;
142
- if (options.signer) {
143
- for (const scope of options.scopes) {
144
- const accessLevel = scope.startsWith('write:') ? 'write' : 'read';
145
- lastDigest = await this.crossContextPermissions.grantWalletAllowlistAccess(
146
- {
147
- requestingWallet,
148
- targetWallet,
149
- scope,
150
- accessLevel,
151
- expiresAt: options.expiresAt ?? 0,
152
- },
153
- options.signer,
154
- );
155
- }
156
- }
157
-
158
- const permissions = await this.crossContextPermissions.queryWalletPermissions({
159
- requestingWallet,
160
- targetWallet,
161
- });
162
-
163
- const grantFromChain = this.buildGrantFromPermissions(
164
- requestingWallet,
165
- targetWallet,
166
- permissions,
167
- );
168
-
169
- const now = Date.now();
170
- let grant: AccessGrant =
171
- grantFromChain ?? {
172
- id: `grant_${requestingWallet}_${targetWallet}_${now}`,
173
- requestingWallet,
174
- targetWallet,
175
- scopes: options.scopes,
176
- expiresAt: options.expiresAt,
177
- grantedAt: now,
178
- };
179
-
180
- if (lastDigest) {
181
- grant = {
182
- ...grant,
183
- transactionDigest: lastDigest,
184
- };
185
- }
186
- await this.updateConsentStatus({
187
- requesterWallet: requestingWallet,
188
- targetWallet,
189
- newStatus: 'approved',
190
- updatedAt: now,
191
- });
192
-
193
- return grant;
194
- }
195
-
196
- /**
197
- * Revoke permissions from an app
198
- * @param userAddress - User revoking permissions
199
- * @param options - Revoke options
200
- * @returns Success status
201
- */
202
- async revokePermissions(
203
- userAddress: string,
204
- options: RevokePermissionsOptions & { signer?: Signer; appId?: string }
205
- ): Promise<boolean> {
206
- const requestingWallet = normalizeSuiAddress(options.requestingWallet);
207
- const targetWallet = normalizeSuiAddress(options.targetWallet);
208
-
209
- // Validate ownership - prefer capability-based validation
210
- const hasAccess = await this.validateOwnership(userAddress, targetWallet, options.appId);
211
- if (!hasAccess) {
212
- throw new Error('User does not own the specified target wallet or capability');
213
- }
214
-
215
- if (options.signer) {
216
- await this.crossContextPermissions.revokeWalletAllowlistAccess(
217
- {
218
- requestingWallet,
219
- targetWallet,
220
- scope: options.scope,
221
- },
222
- options.signer,
223
- );
224
- }
225
-
226
- return true;
227
- }
228
-
229
- /**
230
- * Determine if a requesting wallet currently has permission to access a target wallet
231
- */
232
- async hasWalletPermission(
233
- requestingWallet: string,
234
- targetWallet: string,
235
- scope: PermissionScope
236
- ): Promise<boolean> {
237
- return await this.crossContextPermissions.hasWalletPermission({
238
- requestingWallet: normalizeSuiAddress(requestingWallet),
239
- targetWallet: normalizeSuiAddress(targetWallet),
240
- scope,
241
- });
242
- }
243
-
244
- /**
245
- * Legacy compatibility method for app-scoped permission checks
246
- * Interprets appId as requesting wallet address.
247
- */
248
- async checkPermission(
249
- appId: string,
250
- scope: PermissionScope,
251
- userAddressOrTargetWallet: string
252
- ): Promise<boolean> {
253
- return await this.hasWalletPermission(appId, userAddressOrTargetWallet, scope);
254
- }
255
-
256
- /**
257
- * Get all access grants by a user
258
- * @param userAddress - User address
259
- * @returns Array of access grants
260
- */
261
- async getGrantsByUser(userAddress: string): Promise<AccessGrant[]> {
262
- const normalized = normalizeSuiAddress(userAddress);
263
- const permissions = await this.crossContextPermissions.listGrantsByTarget(normalized);
264
- return this.convertPermissionsToGrants(permissions);
265
- }
266
-
267
- /**
268
- * List all consent requests for a user
269
- * @param userAddress - User address
270
- * @returns Array of pending consent requests
271
- */
272
- async getPendingConsents(userAddress: string): Promise<ConsentRequest[]> {
273
- const normalized = normalizeSuiAddress(userAddress);
274
- if (this.consentRepository) {
275
- return await this.consentRepository.listByTarget(normalized, 'pending');
276
- }
277
- return Array.from(this.pendingConsents.values())
278
- .filter((request) => request.targetWallet === normalized && request.status === 'pending')
279
- .map((request) => ({ ...request }));
280
- }
281
-
282
- /**
283
- * Approve a consent request
284
- * @param userAddress - User approving the request
285
- * @param consentRequest - Consent request to approve
286
- * @param contextId - Context ID to grant access to
287
- * @returns Created access grant
288
- */
289
- async approveConsent(
290
- userAddress: string,
291
- consentRequest: ConsentRequest,
292
- _contextId: string
293
- ): Promise<AccessGrant> {
294
- const grant = await this.grantPermissions(userAddress, {
295
- requestingWallet: consentRequest.requesterWallet,
296
- targetWallet: consentRequest.targetWallet,
297
- scopes: consentRequest.targetScopes,
298
- expiresAt: consentRequest.expiresAt
299
- });
300
-
301
- const updatedAt = Date.now();
302
- if (consentRequest.requestId) {
303
- await this.updateConsentStatus({
304
- requesterWallet: consentRequest.requesterWallet,
305
- targetWallet: consentRequest.targetWallet,
306
- newStatus: 'approved',
307
- updatedAt,
308
- requestId: consentRequest.requestId,
309
- });
310
- }
311
-
312
- return grant;
313
- }
314
-
315
- /**
316
- * Deny a consent request
317
- * @param userAddress - User denying the request
318
- * @param consentRequest - Consent request to deny
319
- * @returns Success status
320
- */
321
- async denyConsent(userAddress: string, consentRequest: ConsentRequest): Promise<boolean> {
322
- const now = Date.now();
323
- if (consentRequest.requestId) {
324
- await this.updateConsentStatus({
325
- requesterWallet: consentRequest.requesterWallet,
326
- targetWallet: consentRequest.targetWallet,
327
- newStatus: 'denied',
328
- updatedAt: now,
329
- requestId: consentRequest.requestId,
330
- });
331
- } else {
332
- const inferredRequestId = randomUUID();
333
- await this.persistConsentRequest({
334
- ...consentRequest,
335
- requestId: inferredRequestId,
336
- createdAt: now,
337
- updatedAt: now,
338
- status: 'denied',
339
- });
340
- }
341
-
342
- return true;
343
- }
344
-
345
- /**
346
- * Get permission audit log for a user
347
- * @param userAddress - User address
348
- * @returns Array of permission events
349
- */
350
- async getPermissionAudit(userAddress: string): Promise<Array<{
351
- timestamp: number;
352
- action: 'grant' | 'revoke' | 'request' | 'deny';
353
- requestingWallet: string;
354
- targetWallet: string;
355
- scopes: PermissionScope[];
356
- }>> {
357
- const normalized = normalizeSuiAddress(userAddress);
358
- const auditEntries: Array<{
359
- timestamp: number;
360
- action: 'grant' | 'revoke' | 'request' | 'deny';
361
- requestingWallet: string;
362
- targetWallet: string;
363
- scopes: PermissionScope[];
364
- }> = [];
365
-
366
- const consentRecords = this.consentRepository
367
- ? await this.consentRepository.listByTarget(normalized)
368
- : Array.from(this.pendingConsents.values()).filter((request) => request.targetWallet === normalized);
369
-
370
- for (const request of consentRecords) {
371
- auditEntries.push({
372
- timestamp: request.createdAt,
373
- action: 'request',
374
- requestingWallet: request.requesterWallet,
375
- targetWallet: request.targetWallet,
376
- scopes: request.targetScopes,
377
- });
378
-
379
- if (request.status === 'denied') {
380
- auditEntries.push({
381
- timestamp: request.updatedAt,
382
- action: 'deny',
383
- requestingWallet: request.requesterWallet,
384
- targetWallet: request.targetWallet,
385
- scopes: request.targetScopes,
386
- });
387
- }
388
- }
389
-
390
- const history = await this.crossContextPermissions.getWalletAllowlistHistory({
391
- targetWallet: normalized,
392
- });
393
-
394
- for (const event of history) {
395
- auditEntries.push({
396
- timestamp: event.timestamp,
397
- action: event.action,
398
- requestingWallet: event.requestingWallet,
399
- targetWallet: event.targetWallet,
400
- scopes: [this.toPermissionScope(event.scope)],
401
- });
402
- }
403
-
404
- return auditEntries.sort((a, b) => a.timestamp - b.timestamp);
405
- }
406
-
407
- /**
408
- * Validate OAuth-style permission for SEAL access control
409
- * @param walletOwner - Owner of the wallet
410
- * @param appId - Application requesting access
411
- * @param requestedScope - Required permission scope
412
- * @returns True if permission is valid
413
- */
414
- async validateOAuthPermission(
415
- walletOwner: string,
416
- appId: string,
417
- requestedScope: string
418
- ): Promise<boolean> {
419
- // This integrates with our existing SEAL access control
420
- return await this.hasWalletPermission(appId, walletOwner, requestedScope as PermissionScope);
421
- }
422
-
423
- /**
424
- * Build seal_approve transaction for a requesting wallet
425
- */
426
- createApprovalTransaction(
427
- contentId: Uint8Array,
428
- requestingWallet: string
429
- ): Transaction {
430
- return this.crossContextPermissions.buildSealApproveTransaction(
431
- contentId,
432
- normalizeSuiAddress(requestingWallet),
433
- );
434
- }
435
-
436
- /**
437
- * Get permission statistics for a user
438
- * @param userAddress - User address
439
- * @returns Permission usage statistics
440
- */
441
- async getPermissionStats(userAddress: string): Promise<{
442
- totalGrants: number;
443
- activeGrants: number;
444
- totalApps: number;
445
- totalScopes: number;
446
- recentActivity: number;
447
- }> {
448
- const grants = await this.getGrantsByUser(userAddress);
449
- const now = Date.now();
450
-
451
- const activeGrants = grants.filter((g) => !g.expiresAt || g.expiresAt > now);
452
- const uniqueApps = new Set(grants.map((g) => g.requestingWallet));
453
- const uniqueScopes = new Set(grants.flatMap((g) => g.scopes));
454
-
455
- const recentActivity =
456
- grants.length > 0 ? Math.max(...grants.map((g) => g.expiresAt || 0)) : 0;
457
-
458
- return {
459
- totalGrants: grants.length,
460
- activeGrants: activeGrants.length,
461
- totalApps: uniqueApps.size,
462
- totalScopes: uniqueScopes.size,
463
- recentActivity
464
- };
465
- }
466
-
467
- /**
468
- * Clean up expired permissions
469
- * @param userAddress - User address
470
- * @returns Number of permissions cleaned up
471
- */
472
- async cleanupExpiredPermissions(userAddress: string): Promise<number> {
473
- const now = Date.now();
474
- const grants = await this.getGrantsByUser(userAddress);
475
-
476
- return grants.filter((grant) => {
477
- if (!grant.expiresAt || grant.expiresAt === 0) {
478
- return false;
479
- }
480
- return grant.expiresAt <= now;
481
- }).length;
482
- }
483
-
484
- private buildGrantFromPermissions(
485
- requestingWallet: string,
486
- targetWallet: string,
487
- permissions: WalletAllowlistPermission[],
488
- ): AccessGrant | undefined {
489
- const grants = this.convertPermissionsToGrants(permissions);
490
- return grants.find(
491
- (candidate) =>
492
- candidate.requestingWallet === requestingWallet &&
493
- candidate.targetWallet === targetWallet,
494
- );
495
- }
496
-
497
- private convertPermissionsToGrants(
498
- permissions: WalletAllowlistPermission[],
499
- ): AccessGrant[] {
500
- const grouped = new Map<string, WalletAllowlistPermission[]>();
501
-
502
- for (const permission of permissions) {
503
- const key = `${permission.requestingWallet}-${permission.targetWallet}`;
504
- const existing = grouped.get(key);
505
- if (existing) {
506
- existing.push(permission);
507
- } else {
508
- grouped.set(key, [permission]);
509
- }
510
- }
511
-
512
- const grants: AccessGrant[] = [];
513
- for (const entries of grouped.values()) {
514
- if (entries.length === 0) {
515
- continue;
516
- }
517
-
518
- const [first] = entries;
519
- const scopes = new Set<PermissionScope>();
520
- let earliestGrantedAt = entries[0].grantedAt;
521
- let expiresAtCandidate = 0;
522
-
523
- for (const entry of entries) {
524
- scopes.add(this.toPermissionScope(entry.scope));
525
- if (entry.grantedAt < earliestGrantedAt) {
526
- earliestGrantedAt = entry.grantedAt;
527
- }
528
- if (entry.expiresAt > expiresAtCandidate) {
529
- expiresAtCandidate = entry.expiresAt;
530
- }
531
- }
532
-
533
- const grant: AccessGrant = {
534
- id: `grant_${first.requestingWallet}_${first.targetWallet}_${earliestGrantedAt}`,
535
- requestingWallet: first.requestingWallet,
536
- targetWallet: first.targetWallet,
537
- scopes: Array.from(scopes),
538
- grantedAt: earliestGrantedAt,
539
- };
540
-
541
- if (expiresAtCandidate > 0) {
542
- grant.expiresAt = expiresAtCandidate;
543
- }
544
-
545
- grants.push(grant);
546
- }
547
-
548
- return grants;
549
- }
550
-
551
- private toPermissionScope(scope: string): PermissionScope {
552
- return scope as PermissionScope;
553
- }
554
-
555
- private async persistConsentRequest(record: ConsentRequestRecord): Promise<void> {
556
- if (this.consentRepository) {
557
- await this.consentRepository.save(record);
558
- } else {
559
- this.pendingConsents.set(record.requestId, { ...record });
560
- }
561
- }
562
-
563
- /**
564
- * Swap the consent persistence backend at runtime.
565
- * Useful for applications that want to wire a custom repository after
566
- * the service has been constructed (e.g., demos supplying a filesystem store).
567
- */
568
- setConsentRepository(repository?: ConsentRepository): void {
569
- this.consentRepository = repository;
570
- }
571
-
572
- /**
573
- * Validate user ownership of a target wallet or capability
574
- * Prefers capability-based validation over legacy ContextWalletService
575
- *
576
- * @param userAddress - User's address
577
- * @param targetWallet - Target wallet address
578
- * @param appId - Optional app ID for capability lookup
579
- * @returns True if user has ownership/access
580
- */
581
- private async validateOwnership(
582
- userAddress: string,
583
- targetWallet: string,
584
- appId?: string
585
- ): Promise<boolean> {
586
- // Prefer capability-based validation (new architecture)
587
- if (this.capabilityService && appId) {
588
- return await this.capabilityService.hasCapability(userAddress, appId);
589
- }
590
-
591
- // Fall back to legacy ContextWalletService validation
592
- if (this.contextWalletService) {
593
- return await this.contextWalletService.validateAccess(targetWallet, userAddress);
594
- }
595
-
596
- // If neither service is available, skip validation (trust caller)
597
- return true;
598
- }
599
-
600
- private async updateConsentStatus(params: {
601
- requesterWallet: string;
602
- targetWallet: string;
603
- newStatus: ConsentStatus;
604
- updatedAt: number;
605
- requestId?: string;
606
- }): Promise<void> {
607
- const normalizedRequester = normalizeSuiAddress(params.requesterWallet);
608
- const normalizedTarget = normalizeSuiAddress(params.targetWallet);
609
-
610
- if (this.consentRepository) {
611
- if (params.requestId) {
612
- await this.consentRepository.updateStatus(params.requestId, params.newStatus, params.updatedAt);
613
- } else {
614
- const pendingRecords = await this.consentRepository.listByTarget(normalizedTarget, 'pending');
615
- await Promise.all(
616
- pendingRecords
617
- .filter((record) => record.requesterWallet === normalizedRequester)
618
- .map((record) =>
619
- this.consentRepository!.updateStatus(record.requestId, params.newStatus, params.updatedAt),
620
- ),
621
- );
622
- }
623
- }
624
-
625
- for (const [requestId, record] of this.pendingConsents.entries()) {
626
- if (record.requesterWallet === normalizedRequester && record.targetWallet === normalizedTarget) {
627
- const updatedRecord: ConsentRequestRecord = {
628
- ...record,
629
- status: params.newStatus,
630
- updatedAt: params.updatedAt,
631
- };
632
- this.pendingConsents.set(requestId, updatedRecord);
633
- }
634
- }
635
- }
1
+ /**
2
+ * PermissionService - OAuth-style access control management
3
+ *
4
+ * Manages permissions for cross-app data access, including:
5
+ * - OAuth-style consent requests and grants
6
+ * - Permission validation and enforcement
7
+ * - On-chain access control integration
8
+ * - Permission auditing and revocation
9
+ */
10
+
11
+ // Use Web Crypto API (browser-compatible)
12
+ const randomUUID = (): string => crypto.randomUUID();
13
+ import { SuiClient } from '@mysten/sui/client';
14
+ import { Transaction } from '@mysten/sui/transactions';
15
+ import { normalizeSuiAddress } from '@mysten/sui/utils';
16
+ import type { Signer } from '@mysten/sui/cryptography';
17
+
18
+ import {
19
+ ConsentRequest,
20
+ ConsentRequestRecord,
21
+ ConsentStatus,
22
+ AccessGrant,
23
+ PermissionScope,
24
+ RequestConsentOptions,
25
+ GrantPermissionsOptions,
26
+ RevokePermissionsOptions
27
+ } from '../core/types/wallet.js';
28
+ import { ContextWalletService } from '../wallet/ContextWalletService.js';
29
+ import { CapabilityService } from '../services/CapabilityService.js';
30
+ import { CrossContextPermissionService } from '../services/CrossContextPermissionService';
31
+ import type { WalletAllowlistPermission } from '../services/CrossContextPermissionService';
32
+ import type { ConsentRepository } from '../permissions/ConsentRepository.js';
33
+
34
+ /**
35
+ * Configuration for PermissionService
36
+ */
37
+ export interface PermissionServiceConfig {
38
+ /** Sui client instance */
39
+ suiClient: SuiClient;
40
+ /** Package ID for Move contracts */
41
+ packageId: string;
42
+ /** Access registry ID for wallet allowlists */
43
+ accessRegistryId: string;
44
+ /**
45
+ * @deprecated Use capabilityService instead for capability-based architecture
46
+ * ContextWalletService for validation (legacy)
47
+ */
48
+ contextWalletService?: ContextWalletService;
49
+ /** CapabilityService for capability-based validation (preferred) */
50
+ capabilityService?: CapabilityService;
51
+ /** Optional injected cross-context permission service */
52
+ crossContextPermissionService?: CrossContextPermissionService;
53
+ /** Optional repository for consent persistence */
54
+ consentRepository?: ConsentRepository;
55
+ }
56
+
57
+ /**
58
+ * PermissionService handles OAuth-style access control
59
+ */
60
+ export class PermissionService {
61
+ private suiClient: SuiClient;
62
+ private packageId: string;
63
+ /** @deprecated Use capabilityService instead */
64
+ private contextWalletService?: ContextWalletService;
65
+ private capabilityService?: CapabilityService;
66
+ private crossContextPermissions: CrossContextPermissionService;
67
+ private pendingConsents: Map<string, ConsentRequestRecord> = new Map();
68
+ private consentRepository?: ConsentRepository;
69
+
70
+ constructor(config: PermissionServiceConfig) {
71
+ this.suiClient = config.suiClient;
72
+ this.packageId = config.packageId;
73
+ this.contextWalletService = config.contextWalletService;
74
+ this.capabilityService = config.capabilityService;
75
+ this.consentRepository = config.consentRepository;
76
+
77
+ if (!config.accessRegistryId) {
78
+ throw new Error('PermissionService requires accessRegistryId for wallet-based permissions');
79
+ }
80
+
81
+ this.crossContextPermissions =
82
+ config.crossContextPermissionService ??
83
+ new CrossContextPermissionService(
84
+ {
85
+ packageId: this.packageId,
86
+ accessRegistryId: config.accessRegistryId,
87
+ },
88
+ this.suiClient,
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Request user consent for accessing data
94
+ * @param options - Consent request options
95
+ * @returns Created consent request
96
+ */
97
+ async requestConsent(options: RequestConsentOptions): Promise<ConsentRequestRecord> {
98
+ const requesterWallet = normalizeSuiAddress(options.requesterWallet);
99
+ const targetWallet = normalizeSuiAddress(options.targetWallet);
100
+ const now = Date.now();
101
+
102
+ const requestId = randomUUID();
103
+ const consentRequest: ConsentRequestRecord = {
104
+ requesterWallet,
105
+ targetWallet,
106
+ targetScopes: options.scopes,
107
+ purpose: options.purpose,
108
+ expiresAt: options.expiresIn ? now + options.expiresIn : undefined,
109
+ requestId,
110
+ createdAt: now,
111
+ updatedAt: now,
112
+ status: 'pending',
113
+ };
114
+
115
+ await this.persistConsentRequest(consentRequest);
116
+
117
+ // TODO: Send consent request to backend for UI presentation
118
+ // For now, return the persisted record so callers can track status locally
119
+ return consentRequest;
120
+ }
121
+
122
+ /**
123
+ * Grant permissions to an app (user approval)
124
+ * @param userAddress - User granting permissions
125
+ * @param options - Grant options
126
+ * @returns Created access grant
127
+ */
128
+ async grantPermissions(
129
+ userAddress: string,
130
+ options: GrantPermissionsOptions & { signer?: Signer; appId?: string }
131
+ ): Promise<AccessGrant> {
132
+ const requestingWallet = normalizeSuiAddress(options.requestingWallet);
133
+ const targetWallet = normalizeSuiAddress(options.targetWallet);
134
+
135
+ // Validate ownership - prefer capability-based validation
136
+ const hasAccess = await this.validateOwnership(userAddress, targetWallet, options.appId);
137
+ if (!hasAccess) {
138
+ throw new Error('User does not own the specified target wallet or capability');
139
+ }
140
+
141
+ let lastDigest: string | undefined;
142
+ if (options.signer) {
143
+ for (const scope of options.scopes) {
144
+ const accessLevel = scope.startsWith('write:') ? 'write' : 'read';
145
+ lastDigest = await this.crossContextPermissions.grantWalletAllowlistAccess(
146
+ {
147
+ requestingWallet,
148
+ targetWallet,
149
+ scope,
150
+ accessLevel,
151
+ expiresAt: options.expiresAt ?? 0,
152
+ },
153
+ options.signer,
154
+ );
155
+ }
156
+ }
157
+
158
+ const permissions = await this.crossContextPermissions.queryWalletPermissions({
159
+ requestingWallet,
160
+ targetWallet,
161
+ });
162
+
163
+ const grantFromChain = this.buildGrantFromPermissions(
164
+ requestingWallet,
165
+ targetWallet,
166
+ permissions,
167
+ );
168
+
169
+ const now = Date.now();
170
+ let grant: AccessGrant =
171
+ grantFromChain ?? {
172
+ id: `grant_${requestingWallet}_${targetWallet}_${now}`,
173
+ requestingWallet,
174
+ targetWallet,
175
+ scopes: options.scopes,
176
+ expiresAt: options.expiresAt,
177
+ grantedAt: now,
178
+ };
179
+
180
+ if (lastDigest) {
181
+ grant = {
182
+ ...grant,
183
+ transactionDigest: lastDigest,
184
+ };
185
+ }
186
+ await this.updateConsentStatus({
187
+ requesterWallet: requestingWallet,
188
+ targetWallet,
189
+ newStatus: 'approved',
190
+ updatedAt: now,
191
+ });
192
+
193
+ return grant;
194
+ }
195
+
196
+ /**
197
+ * Revoke permissions from an app
198
+ * @param userAddress - User revoking permissions
199
+ * @param options - Revoke options
200
+ * @returns Success status
201
+ */
202
+ async revokePermissions(
203
+ userAddress: string,
204
+ options: RevokePermissionsOptions & { signer?: Signer; appId?: string }
205
+ ): Promise<boolean> {
206
+ const requestingWallet = normalizeSuiAddress(options.requestingWallet);
207
+ const targetWallet = normalizeSuiAddress(options.targetWallet);
208
+
209
+ // Validate ownership - prefer capability-based validation
210
+ const hasAccess = await this.validateOwnership(userAddress, targetWallet, options.appId);
211
+ if (!hasAccess) {
212
+ throw new Error('User does not own the specified target wallet or capability');
213
+ }
214
+
215
+ if (options.signer) {
216
+ await this.crossContextPermissions.revokeWalletAllowlistAccess(
217
+ {
218
+ requestingWallet,
219
+ targetWallet,
220
+ scope: options.scope,
221
+ },
222
+ options.signer,
223
+ );
224
+ }
225
+
226
+ return true;
227
+ }
228
+
229
+ /**
230
+ * Determine if a requesting wallet currently has permission to access a target wallet
231
+ */
232
+ async hasWalletPermission(
233
+ requestingWallet: string,
234
+ targetWallet: string,
235
+ scope: PermissionScope
236
+ ): Promise<boolean> {
237
+ return await this.crossContextPermissions.hasWalletPermission({
238
+ requestingWallet: normalizeSuiAddress(requestingWallet),
239
+ targetWallet: normalizeSuiAddress(targetWallet),
240
+ scope,
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Legacy compatibility method for app-scoped permission checks
246
+ * Interprets appId as requesting wallet address.
247
+ */
248
+ async checkPermission(
249
+ appId: string,
250
+ scope: PermissionScope,
251
+ userAddressOrTargetWallet: string
252
+ ): Promise<boolean> {
253
+ return await this.hasWalletPermission(appId, userAddressOrTargetWallet, scope);
254
+ }
255
+
256
+ /**
257
+ * Get all access grants by a user
258
+ * @param userAddress - User address
259
+ * @returns Array of access grants
260
+ */
261
+ async getGrantsByUser(userAddress: string): Promise<AccessGrant[]> {
262
+ const normalized = normalizeSuiAddress(userAddress);
263
+ const permissions = await this.crossContextPermissions.listGrantsByTarget(normalized);
264
+ return this.convertPermissionsToGrants(permissions);
265
+ }
266
+
267
+ /**
268
+ * List all consent requests for a user
269
+ * @param userAddress - User address
270
+ * @returns Array of pending consent requests
271
+ */
272
+ async getPendingConsents(userAddress: string): Promise<ConsentRequest[]> {
273
+ const normalized = normalizeSuiAddress(userAddress);
274
+ if (this.consentRepository) {
275
+ return await this.consentRepository.listByTarget(normalized, 'pending');
276
+ }
277
+ return Array.from(this.pendingConsents.values())
278
+ .filter((request) => request.targetWallet === normalized && request.status === 'pending')
279
+ .map((request) => ({ ...request }));
280
+ }
281
+
282
+ /**
283
+ * Approve a consent request
284
+ * @param userAddress - User approving the request
285
+ * @param consentRequest - Consent request to approve
286
+ * @param contextId - Context ID to grant access to
287
+ * @returns Created access grant
288
+ */
289
+ async approveConsent(
290
+ userAddress: string,
291
+ consentRequest: ConsentRequest,
292
+ _contextId: string
293
+ ): Promise<AccessGrant> {
294
+ const grant = await this.grantPermissions(userAddress, {
295
+ requestingWallet: consentRequest.requesterWallet,
296
+ targetWallet: consentRequest.targetWallet,
297
+ scopes: consentRequest.targetScopes,
298
+ expiresAt: consentRequest.expiresAt
299
+ });
300
+
301
+ const updatedAt = Date.now();
302
+ if (consentRequest.requestId) {
303
+ await this.updateConsentStatus({
304
+ requesterWallet: consentRequest.requesterWallet,
305
+ targetWallet: consentRequest.targetWallet,
306
+ newStatus: 'approved',
307
+ updatedAt,
308
+ requestId: consentRequest.requestId,
309
+ });
310
+ }
311
+
312
+ return grant;
313
+ }
314
+
315
+ /**
316
+ * Deny a consent request
317
+ * @param userAddress - User denying the request
318
+ * @param consentRequest - Consent request to deny
319
+ * @returns Success status
320
+ */
321
+ async denyConsent(userAddress: string, consentRequest: ConsentRequest): Promise<boolean> {
322
+ const now = Date.now();
323
+ if (consentRequest.requestId) {
324
+ await this.updateConsentStatus({
325
+ requesterWallet: consentRequest.requesterWallet,
326
+ targetWallet: consentRequest.targetWallet,
327
+ newStatus: 'denied',
328
+ updatedAt: now,
329
+ requestId: consentRequest.requestId,
330
+ });
331
+ } else {
332
+ const inferredRequestId = randomUUID();
333
+ await this.persistConsentRequest({
334
+ ...consentRequest,
335
+ requestId: inferredRequestId,
336
+ createdAt: now,
337
+ updatedAt: now,
338
+ status: 'denied',
339
+ });
340
+ }
341
+
342
+ return true;
343
+ }
344
+
345
+ /**
346
+ * Get permission audit log for a user
347
+ * @param userAddress - User address
348
+ * @returns Array of permission events
349
+ */
350
+ async getPermissionAudit(userAddress: string): Promise<Array<{
351
+ timestamp: number;
352
+ action: 'grant' | 'revoke' | 'request' | 'deny';
353
+ requestingWallet: string;
354
+ targetWallet: string;
355
+ scopes: PermissionScope[];
356
+ }>> {
357
+ const normalized = normalizeSuiAddress(userAddress);
358
+ const auditEntries: Array<{
359
+ timestamp: number;
360
+ action: 'grant' | 'revoke' | 'request' | 'deny';
361
+ requestingWallet: string;
362
+ targetWallet: string;
363
+ scopes: PermissionScope[];
364
+ }> = [];
365
+
366
+ const consentRecords = this.consentRepository
367
+ ? await this.consentRepository.listByTarget(normalized)
368
+ : Array.from(this.pendingConsents.values()).filter((request) => request.targetWallet === normalized);
369
+
370
+ for (const request of consentRecords) {
371
+ auditEntries.push({
372
+ timestamp: request.createdAt,
373
+ action: 'request',
374
+ requestingWallet: request.requesterWallet,
375
+ targetWallet: request.targetWallet,
376
+ scopes: request.targetScopes,
377
+ });
378
+
379
+ if (request.status === 'denied') {
380
+ auditEntries.push({
381
+ timestamp: request.updatedAt,
382
+ action: 'deny',
383
+ requestingWallet: request.requesterWallet,
384
+ targetWallet: request.targetWallet,
385
+ scopes: request.targetScopes,
386
+ });
387
+ }
388
+ }
389
+
390
+ const history = await this.crossContextPermissions.getWalletAllowlistHistory({
391
+ targetWallet: normalized,
392
+ });
393
+
394
+ for (const event of history) {
395
+ auditEntries.push({
396
+ timestamp: event.timestamp,
397
+ action: event.action,
398
+ requestingWallet: event.requestingWallet,
399
+ targetWallet: event.targetWallet,
400
+ scopes: [this.toPermissionScope(event.scope)],
401
+ });
402
+ }
403
+
404
+ return auditEntries.sort((a, b) => a.timestamp - b.timestamp);
405
+ }
406
+
407
+ /**
408
+ * Validate OAuth-style permission for SEAL access control
409
+ * @param walletOwner - Owner of the wallet
410
+ * @param appId - Application requesting access
411
+ * @param requestedScope - Required permission scope
412
+ * @returns True if permission is valid
413
+ */
414
+ async validateOAuthPermission(
415
+ walletOwner: string,
416
+ appId: string,
417
+ requestedScope: string
418
+ ): Promise<boolean> {
419
+ // This integrates with our existing SEAL access control
420
+ return await this.hasWalletPermission(appId, walletOwner, requestedScope as PermissionScope);
421
+ }
422
+
423
+ /**
424
+ * Build seal_approve transaction for a requesting wallet
425
+ */
426
+ createApprovalTransaction(
427
+ contentId: Uint8Array,
428
+ requestingWallet: string
429
+ ): Transaction {
430
+ return this.crossContextPermissions.buildSealApproveTransaction(
431
+ contentId,
432
+ normalizeSuiAddress(requestingWallet),
433
+ );
434
+ }
435
+
436
+ /**
437
+ * Get permission statistics for a user
438
+ * @param userAddress - User address
439
+ * @returns Permission usage statistics
440
+ */
441
+ async getPermissionStats(userAddress: string): Promise<{
442
+ totalGrants: number;
443
+ activeGrants: number;
444
+ totalApps: number;
445
+ totalScopes: number;
446
+ recentActivity: number;
447
+ }> {
448
+ const grants = await this.getGrantsByUser(userAddress);
449
+ const now = Date.now();
450
+
451
+ const activeGrants = grants.filter((g) => !g.expiresAt || g.expiresAt > now);
452
+ const uniqueApps = new Set(grants.map((g) => g.requestingWallet));
453
+ const uniqueScopes = new Set(grants.flatMap((g) => g.scopes));
454
+
455
+ const recentActivity =
456
+ grants.length > 0 ? Math.max(...grants.map((g) => g.expiresAt || 0)) : 0;
457
+
458
+ return {
459
+ totalGrants: grants.length,
460
+ activeGrants: activeGrants.length,
461
+ totalApps: uniqueApps.size,
462
+ totalScopes: uniqueScopes.size,
463
+ recentActivity
464
+ };
465
+ }
466
+
467
+ /**
468
+ * Clean up expired permissions
469
+ * @param userAddress - User address
470
+ * @returns Number of permissions cleaned up
471
+ */
472
+ async cleanupExpiredPermissions(userAddress: string): Promise<number> {
473
+ const now = Date.now();
474
+ const grants = await this.getGrantsByUser(userAddress);
475
+
476
+ return grants.filter((grant) => {
477
+ if (!grant.expiresAt || grant.expiresAt === 0) {
478
+ return false;
479
+ }
480
+ return grant.expiresAt <= now;
481
+ }).length;
482
+ }
483
+
484
+ private buildGrantFromPermissions(
485
+ requestingWallet: string,
486
+ targetWallet: string,
487
+ permissions: WalletAllowlistPermission[],
488
+ ): AccessGrant | undefined {
489
+ const grants = this.convertPermissionsToGrants(permissions);
490
+ return grants.find(
491
+ (candidate) =>
492
+ candidate.requestingWallet === requestingWallet &&
493
+ candidate.targetWallet === targetWallet,
494
+ );
495
+ }
496
+
497
+ private convertPermissionsToGrants(
498
+ permissions: WalletAllowlistPermission[],
499
+ ): AccessGrant[] {
500
+ const grouped = new Map<string, WalletAllowlistPermission[]>();
501
+
502
+ for (const permission of permissions) {
503
+ const key = `${permission.requestingWallet}-${permission.targetWallet}`;
504
+ const existing = grouped.get(key);
505
+ if (existing) {
506
+ existing.push(permission);
507
+ } else {
508
+ grouped.set(key, [permission]);
509
+ }
510
+ }
511
+
512
+ const grants: AccessGrant[] = [];
513
+ for (const entries of grouped.values()) {
514
+ if (entries.length === 0) {
515
+ continue;
516
+ }
517
+
518
+ const [first] = entries;
519
+ const scopes = new Set<PermissionScope>();
520
+ let earliestGrantedAt = entries[0].grantedAt;
521
+ let expiresAtCandidate = 0;
522
+
523
+ for (const entry of entries) {
524
+ scopes.add(this.toPermissionScope(entry.scope));
525
+ if (entry.grantedAt < earliestGrantedAt) {
526
+ earliestGrantedAt = entry.grantedAt;
527
+ }
528
+ if (entry.expiresAt > expiresAtCandidate) {
529
+ expiresAtCandidate = entry.expiresAt;
530
+ }
531
+ }
532
+
533
+ const grant: AccessGrant = {
534
+ id: `grant_${first.requestingWallet}_${first.targetWallet}_${earliestGrantedAt}`,
535
+ requestingWallet: first.requestingWallet,
536
+ targetWallet: first.targetWallet,
537
+ scopes: Array.from(scopes),
538
+ grantedAt: earliestGrantedAt,
539
+ };
540
+
541
+ if (expiresAtCandidate > 0) {
542
+ grant.expiresAt = expiresAtCandidate;
543
+ }
544
+
545
+ grants.push(grant);
546
+ }
547
+
548
+ return grants;
549
+ }
550
+
551
+ private toPermissionScope(scope: string): PermissionScope {
552
+ return scope as PermissionScope;
553
+ }
554
+
555
+ private async persistConsentRequest(record: ConsentRequestRecord): Promise<void> {
556
+ if (this.consentRepository) {
557
+ await this.consentRepository.save(record);
558
+ } else {
559
+ this.pendingConsents.set(record.requestId, { ...record });
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Swap the consent persistence backend at runtime.
565
+ * Useful for applications that want to wire a custom repository after
566
+ * the service has been constructed (e.g., demos supplying a filesystem store).
567
+ */
568
+ setConsentRepository(repository?: ConsentRepository): void {
569
+ this.consentRepository = repository;
570
+ }
571
+
572
+ /**
573
+ * Validate user ownership of a target wallet or capability
574
+ * Prefers capability-based validation over legacy ContextWalletService
575
+ *
576
+ * @param userAddress - User's address
577
+ * @param targetWallet - Target wallet address
578
+ * @param appId - Optional app ID for capability lookup
579
+ * @returns True if user has ownership/access
580
+ */
581
+ private async validateOwnership(
582
+ userAddress: string,
583
+ targetWallet: string,
584
+ appId?: string
585
+ ): Promise<boolean> {
586
+ // Prefer capability-based validation (new architecture)
587
+ if (this.capabilityService && appId) {
588
+ return await this.capabilityService.hasCapability(userAddress, appId);
589
+ }
590
+
591
+ // Fall back to legacy ContextWalletService validation
592
+ if (this.contextWalletService) {
593
+ return await this.contextWalletService.validateAccess(targetWallet, userAddress);
594
+ }
595
+
596
+ // If neither service is available, skip validation (trust caller)
597
+ return true;
598
+ }
599
+
600
+ private async updateConsentStatus(params: {
601
+ requesterWallet: string;
602
+ targetWallet: string;
603
+ newStatus: ConsentStatus;
604
+ updatedAt: number;
605
+ requestId?: string;
606
+ }): Promise<void> {
607
+ const normalizedRequester = normalizeSuiAddress(params.requesterWallet);
608
+ const normalizedTarget = normalizeSuiAddress(params.targetWallet);
609
+
610
+ if (this.consentRepository) {
611
+ if (params.requestId) {
612
+ await this.consentRepository.updateStatus(params.requestId, params.newStatus, params.updatedAt);
613
+ } else {
614
+ const pendingRecords = await this.consentRepository.listByTarget(normalizedTarget, 'pending');
615
+ await Promise.all(
616
+ pendingRecords
617
+ .filter((record) => record.requesterWallet === normalizedRequester)
618
+ .map((record) =>
619
+ this.consentRepository!.updateStatus(record.requestId, params.newStatus, params.updatedAt),
620
+ ),
621
+ );
622
+ }
623
+ }
624
+
625
+ for (const [requestId, record] of this.pendingConsents.entries()) {
626
+ if (record.requesterWallet === normalizedRequester && record.targetWallet === normalizedTarget) {
627
+ const updatedRecord: ConsentRequestRecord = {
628
+ ...record,
629
+ status: params.newStatus,
630
+ updatedAt: params.updatedAt,
631
+ };
632
+ this.pendingConsents.set(requestId, updatedRecord);
633
+ }
634
+ }
635
+ }
636
636
  }