@solana/mpp 0.1.1 → 0.2.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 (91) hide show
  1. package/dist/Methods.d.ts +1 -213
  2. package/dist/Methods.d.ts.map +1 -1
  3. package/dist/Methods.js +1 -158
  4. package/dist/Methods.js.map +1 -1
  5. package/dist/client/Methods.d.ts +0 -2
  6. package/dist/client/Methods.d.ts.map +1 -1
  7. package/dist/client/Methods.js +0 -2
  8. package/dist/client/Methods.js.map +1 -1
  9. package/dist/client/index.d.ts +0 -1
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +0 -1
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/index.d.ts +0 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +0 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/server/Charge.d.ts +2 -2
  18. package/dist/server/Charge.d.ts.map +1 -1
  19. package/dist/server/Charge.js +4 -0
  20. package/dist/server/Charge.js.map +1 -1
  21. package/dist/server/Methods.d.ts +0 -2
  22. package/dist/server/Methods.d.ts.map +1 -1
  23. package/dist/server/Methods.js +0 -2
  24. package/dist/server/Methods.js.map +1 -1
  25. package/dist/server/index.d.ts +0 -1
  26. package/dist/server/index.d.ts.map +1 -1
  27. package/dist/server/index.js +0 -1
  28. package/dist/server/index.js.map +1 -1
  29. package/package.json +1 -9
  30. package/src/Methods.ts +1 -171
  31. package/src/client/Methods.ts +0 -3
  32. package/src/client/index.ts +0 -1
  33. package/src/index.ts +0 -29
  34. package/src/server/Charge.ts +7 -2
  35. package/src/server/Methods.ts +0 -3
  36. package/src/server/index.ts +0 -1
  37. package/dist/client/Session.d.ts +0 -195
  38. package/dist/client/Session.d.ts.map +0 -1
  39. package/dist/client/Session.js +0 -411
  40. package/dist/client/Session.js.map +0 -1
  41. package/dist/server/Session.d.ts +0 -171
  42. package/dist/server/Session.d.ts.map +0 -1
  43. package/dist/server/Session.js +0 -430
  44. package/dist/server/Session.js.map +0 -1
  45. package/dist/session/ChannelStore.d.ts +0 -12
  46. package/dist/session/ChannelStore.d.ts.map +0 -1
  47. package/dist/session/ChannelStore.js +0 -88
  48. package/dist/session/ChannelStore.js.map +0 -1
  49. package/dist/session/Types.d.ts +0 -179
  50. package/dist/session/Types.d.ts.map +0 -1
  51. package/dist/session/Types.js +0 -2
  52. package/dist/session/Types.js.map +0 -1
  53. package/dist/session/Voucher.d.ts +0 -7
  54. package/dist/session/Voucher.d.ts.map +0 -1
  55. package/dist/session/Voucher.js +0 -118
  56. package/dist/session/Voucher.js.map +0 -1
  57. package/dist/session/authorizers/BudgetAuthorizer.d.ts +0 -90
  58. package/dist/session/authorizers/BudgetAuthorizer.d.ts.map +0 -1
  59. package/dist/session/authorizers/BudgetAuthorizer.js +0 -398
  60. package/dist/session/authorizers/BudgetAuthorizer.js.map +0 -1
  61. package/dist/session/authorizers/SwigSessionAuthorizer.d.ts +0 -104
  62. package/dist/session/authorizers/SwigSessionAuthorizer.d.ts.map +0 -1
  63. package/dist/session/authorizers/SwigSessionAuthorizer.js +0 -522
  64. package/dist/session/authorizers/SwigSessionAuthorizer.js.map +0 -1
  65. package/dist/session/authorizers/UnboundedAuthorizer.d.ts +0 -36
  66. package/dist/session/authorizers/UnboundedAuthorizer.d.ts.map +0 -1
  67. package/dist/session/authorizers/UnboundedAuthorizer.js +0 -204
  68. package/dist/session/authorizers/UnboundedAuthorizer.js.map +0 -1
  69. package/dist/session/authorizers/index.d.ts +0 -5
  70. package/dist/session/authorizers/index.d.ts.map +0 -1
  71. package/dist/session/authorizers/index.js +0 -5
  72. package/dist/session/authorizers/index.js.map +0 -1
  73. package/dist/session/authorizers/makeSessionAuthorizer.d.ts +0 -19
  74. package/dist/session/authorizers/makeSessionAuthorizer.d.ts.map +0 -1
  75. package/dist/session/authorizers/makeSessionAuthorizer.js +0 -72
  76. package/dist/session/authorizers/makeSessionAuthorizer.js.map +0 -1
  77. package/dist/session/index.d.ts +0 -5
  78. package/dist/session/index.d.ts.map +0 -1
  79. package/dist/session/index.js +0 -5
  80. package/dist/session/index.js.map +0 -1
  81. package/src/client/Session.ts +0 -630
  82. package/src/server/Session.ts +0 -687
  83. package/src/session/ChannelStore.ts +0 -128
  84. package/src/session/Types.ts +0 -189
  85. package/src/session/Voucher.ts +0 -158
  86. package/src/session/authorizers/BudgetAuthorizer.ts +0 -574
  87. package/src/session/authorizers/SwigSessionAuthorizer.ts +0 -767
  88. package/src/session/authorizers/UnboundedAuthorizer.ts +0 -284
  89. package/src/session/authorizers/index.ts +0 -4
  90. package/src/session/authorizers/makeSessionAuthorizer.ts +0 -104
  91. package/src/session/index.ts +0 -4
@@ -1,767 +0,0 @@
1
- import { createSolanaRpc, type KeyPairSigner } from '@solana/kit';
2
-
3
- import { DEFAULT_RPC_URLS } from '../../constants.js';
4
- import {
5
- type AuthorizeCloseInput,
6
- type AuthorizedClose,
7
- type AuthorizedOpen,
8
- type AuthorizedTopup,
9
- type AuthorizedUpdate,
10
- type AuthorizeOpenInput,
11
- type AuthorizerCapabilities,
12
- type AuthorizeTopupInput,
13
- type AuthorizeUpdateInput,
14
- type SessionAuthorizer,
15
- type SessionPolicyProfile,
16
- type SessionVoucher,
17
- type SignedSessionVoucher,
18
- } from '../Types.js';
19
- import { signVoucher } from '../Voucher.js';
20
-
21
- type SwigPolicy = Extract<SessionPolicyProfile, { profile: 'swig-time-bound' }>;
22
-
23
- type SwigRoleActions = {
24
- canUseProgram?: (programId: string) => boolean;
25
- solSpendLimit?: () => bigint | null;
26
- tokenSpendLimit?: (mint: string) => bigint | null;
27
- };
28
-
29
- type SwigRole = {
30
- actions?: SwigRoleActions;
31
- id: number;
32
- };
33
-
34
- type SwigAccount = {
35
- findRoleById?: (id: number) => SwigRole | null;
36
- findRoleBySessionKey?: (sessionKey: string) => SwigRole | null;
37
- };
38
-
39
- export type SwigSessionModule = {
40
- fetchSwig: (rpc: unknown, swigAddress: string) => Promise<SwigAccount>;
41
- };
42
-
43
- type SessionSignerState = {
44
- createdAtMs?: number;
45
- openTx?: string;
46
- signer: KeyPairSigner;
47
- swigRoleId?: number;
48
- };
49
-
50
- type ChannelProgress = {
51
- deposited: bigint;
52
- lastCumulative: bigint;
53
- lastSequence: number;
54
- signerAddress: string;
55
- swigRoleId?: number;
56
- };
57
-
58
- export type SwigSessionKeyResult =
59
- | KeyPairSigner
60
- | {
61
- createdAt?: Date | number | string;
62
- openTx?: string;
63
- signer: KeyPairSigner;
64
- swigRoleId?: number;
65
- };
66
-
67
- export interface SwigWalletAdapter {
68
- address: string;
69
- createSessionKey?: (config: {
70
- channelId: string;
71
- channelProgram: string;
72
- depositLimit?: string;
73
- network: string;
74
- recipient: string;
75
- spendLimit?: string;
76
- ttlSeconds: number;
77
- }) => Promise<SwigSessionKeyResult>;
78
- getSessionKey?: () => Promise<SwigSessionKeyResult | null | undefined>;
79
- swigAddress?: string;
80
- swigRoleId?: number;
81
- }
82
-
83
- export interface SwigSessionAuthorizerParameters {
84
- allowedPrograms?: string[];
85
- buildCloseTx?: (input: AuthorizeCloseInput) => Promise<string> | string;
86
- buildOpenTx?: (input: AuthorizeOpenInput) => Promise<string> | string;
87
- buildTopupTx?: (input: AuthorizeTopupInput) => Promise<string> | string;
88
- policy: SwigPolicy;
89
- rpcUrl?: string;
90
- swigModule?: SwigSessionModule;
91
- wallet: SwigWalletAdapter;
92
- }
93
-
94
- /**
95
- * Session authorizer for `swig_session` mode.
96
- *
97
- * This authorizer binds delegated session keys to on-chain Swig role policy
98
- * before issuing vouchers, then keeps channel state tied to that signer/role.
99
- */
100
- export class SwigSessionAuthorizer implements SessionAuthorizer {
101
- private readonly wallet: SwigWalletAdapter;
102
- private readonly policy: SwigPolicy;
103
- private readonly rpcUrl?: string;
104
- private readonly allowedPrograms?: Set<string>;
105
- private readonly spendLimit?: bigint;
106
- private readonly depositLimit?: bigint;
107
- private readonly buildOpenTx?: (input: AuthorizeOpenInput) => Promise<string> | string;
108
- private readonly buildTopupTx?: (input: AuthorizeTopupInput) => Promise<string> | string;
109
- private readonly buildCloseTx?: (input: AuthorizeCloseInput) => Promise<string> | string;
110
- private readonly channels = new Map<string, ChannelProgress>();
111
-
112
- private swigLoaded = false;
113
- private swigModule: SwigSessionModule | null = null;
114
- private sessionSigner: KeyPairSigner | null = null;
115
- private sessionStartedAtMs: number | null = null;
116
- private sessionOpenTx: string | null = null;
117
- private sessionRoleId: number | null = null;
118
- private validatedPolicyForSessionSigner: string | null = null;
119
-
120
- constructor(parameters: SwigSessionAuthorizerParameters) {
121
- if (!Number.isInteger(parameters.policy.ttlSeconds) || parameters.policy.ttlSeconds <= 0) {
122
- throw new Error('Swig policy `ttlSeconds` must be a positive integer');
123
- }
124
-
125
- this.wallet = parameters.wallet;
126
- this.policy = parameters.policy;
127
- this.rpcUrl = parameters.rpcUrl;
128
- if (parameters.swigModule) {
129
- this.swigModule = parameters.swigModule;
130
- this.swigLoaded = true;
131
- }
132
- this.allowedPrograms = parameters.allowedPrograms ? new Set(parameters.allowedPrograms) : undefined;
133
- this.spendLimit =
134
- this.policy.spendLimit !== undefined
135
- ? parseNonNegativeAmount(this.policy.spendLimit, 'spendLimit')
136
- : undefined;
137
- this.depositLimit =
138
- this.policy.depositLimit !== undefined
139
- ? parseNonNegativeAmount(this.policy.depositLimit, 'depositLimit')
140
- : undefined;
141
- this.buildOpenTx = parameters.buildOpenTx;
142
- this.buildTopupTx = parameters.buildTopupTx;
143
- this.buildCloseTx = parameters.buildCloseTx;
144
- }
145
-
146
- getMode() {
147
- return 'swig_session' as const;
148
- }
149
-
150
- getCapabilities(): AuthorizerCapabilities {
151
- return {
152
- expiresAt: this.getSessionExpiresAt(),
153
- mode: 'swig_session',
154
- ...(this.policy.spendLimit ? { maxCumulativeAmount: this.policy.spendLimit } : {}),
155
- ...(this.policy.depositLimit ? { maxDepositAmount: this.policy.depositLimit } : {}),
156
- ...(this.allowedPrograms ? { allowedPrograms: [...this.allowedPrograms] } : {}),
157
- allowedActions: ['open', 'update', 'topup', 'close'],
158
- requiresInteractiveApproval: {
159
- close: false,
160
- open: true,
161
- topup: !this.policy.autoTopup?.enabled,
162
- update: false,
163
- },
164
- };
165
- }
166
-
167
- async authorizeOpen(input: AuthorizeOpenInput): Promise<AuthorizedOpen> {
168
- await this.ensureSwigInstalled();
169
- this.assertProgramAllowed(input.channelProgram);
170
-
171
- const deposit = parseNonNegativeAmount(input.depositAmount, 'depositAmount');
172
- if (this.depositLimit !== undefined && deposit > this.depositLimit) {
173
- throw new Error(`Open deposit exceeds depositLimit (${this.depositLimit.toString()})`);
174
- }
175
-
176
- const session = await this.ensureSessionSignerForOpen(input);
177
- await this.assertPolicyAppliedOnChain(input, session);
178
-
179
- const sessionSigner = session.signer;
180
- const openTx = await this.resolveOpenTx(input, session);
181
- const expiresAt = this.getSessionExpiresAt();
182
-
183
- const voucher = await this.signSwigVoucher(sessionSigner, {
184
- chainId: normalizeChainId(input.network),
185
- channelId: input.channelId,
186
- channelProgram: input.channelProgram,
187
- cumulativeAmount: '0',
188
- expiresAt,
189
- meter: input.pricing?.meter ?? 'session',
190
- payer: this.wallet.address,
191
- recipient: input.recipient,
192
- sequence: 0,
193
- serverNonce: input.serverNonce,
194
- units: '0',
195
- });
196
-
197
- this.channels.set(input.channelId, {
198
- deposited: deposit,
199
- lastCumulative: 0n,
200
- lastSequence: 0,
201
- signerAddress: sessionSigner.address,
202
- ...(session.swigRoleId !== undefined ? { swigRoleId: session.swigRoleId } : {}),
203
- });
204
-
205
- return {
206
- capabilities: this.getCapabilities(),
207
- expiresAt,
208
- openTx,
209
- voucher,
210
- };
211
- }
212
-
213
- async authorizeUpdate(input: AuthorizeUpdateInput): Promise<AuthorizedUpdate> {
214
- await this.ensureSwigInstalled();
215
- this.assertProgramAllowed(input.channelProgram);
216
-
217
- const cumulativeAmount = parseNonNegativeAmount(input.cumulativeAmount, 'cumulativeAmount');
218
- if (this.spendLimit !== undefined && cumulativeAmount > this.spendLimit) {
219
- throw new Error(`Cumulative amount exceeds spendLimit (${this.spendLimit.toString()})`);
220
- }
221
-
222
- const progress = this.channels.get(input.channelId);
223
- const sessionSigner = this.requireActiveSessionSigner(input.channelId, progress);
224
-
225
- this.assertMonotonic(input.channelId, input.sequence, cumulativeAmount, progress);
226
-
227
- const voucher = await this.signSwigVoucher(sessionSigner, {
228
- chainId: normalizeChainId(input.network),
229
- channelId: input.channelId,
230
- channelProgram: input.channelProgram,
231
- cumulativeAmount: cumulativeAmount.toString(),
232
- expiresAt: this.getSessionExpiresAt(),
233
- meter: input.meter,
234
- payer: this.wallet.address,
235
- recipient: input.recipient,
236
- sequence: input.sequence,
237
- serverNonce: input.serverNonce,
238
- units: input.units,
239
- });
240
-
241
- this.channels.set(input.channelId, {
242
- deposited: progress?.deposited ?? 0n,
243
- lastCumulative: cumulativeAmount,
244
- lastSequence: input.sequence,
245
- signerAddress: sessionSigner.address,
246
- ...(progress?.swigRoleId !== undefined
247
- ? { swigRoleId: progress.swigRoleId }
248
- : this.sessionRoleId !== null
249
- ? { swigRoleId: this.sessionRoleId }
250
- : {}),
251
- });
252
-
253
- return { voucher };
254
- }
255
-
256
- async authorizeTopup(input: AuthorizeTopupInput): Promise<AuthorizedTopup> {
257
- await this.ensureSwigInstalled();
258
- this.assertProgramAllowed(input.channelProgram);
259
-
260
- const progress = this.channels.get(input.channelId);
261
- const sessionSigner = this.requireActiveSessionSigner(input.channelId, progress);
262
- const additionalAmount = parseNonNegativeAmount(input.additionalAmount, 'additionalAmount');
263
-
264
- const nextDeposited = (progress?.deposited ?? 0n) + additionalAmount;
265
- if (this.depositLimit !== undefined && nextDeposited > this.depositLimit) {
266
- throw new Error(`Topup exceeds depositLimit (${this.depositLimit.toString()})`);
267
- }
268
-
269
- const topupTx = await this.resolveTopupTx(input);
270
-
271
- this.channels.set(input.channelId, {
272
- deposited: nextDeposited,
273
- lastCumulative: progress?.lastCumulative ?? 0n,
274
- lastSequence: progress?.lastSequence ?? 0,
275
- signerAddress: sessionSigner.address,
276
- ...(progress?.swigRoleId !== undefined
277
- ? { swigRoleId: progress.swigRoleId }
278
- : this.sessionRoleId !== null
279
- ? { swigRoleId: this.sessionRoleId }
280
- : {}),
281
- });
282
-
283
- return { topupTx };
284
- }
285
-
286
- async authorizeClose(input: AuthorizeCloseInput): Promise<AuthorizedClose> {
287
- await this.ensureSwigInstalled();
288
- this.assertProgramAllowed(input.channelProgram);
289
-
290
- const finalCumulativeAmount = parseNonNegativeAmount(input.finalCumulativeAmount, 'finalCumulativeAmount');
291
- if (this.spendLimit !== undefined && finalCumulativeAmount > this.spendLimit) {
292
- throw new Error(`Final cumulative amount exceeds spendLimit (${this.spendLimit.toString()})`);
293
- }
294
-
295
- const progress = this.channels.get(input.channelId);
296
- const sessionSigner = this.requireActiveSessionSigner(input.channelId, progress);
297
-
298
- this.assertMonotonic(input.channelId, input.sequence, finalCumulativeAmount, progress);
299
-
300
- const voucher = await this.signSwigVoucher(sessionSigner, {
301
- chainId: normalizeChainId(input.network),
302
- channelId: input.channelId,
303
- channelProgram: input.channelProgram,
304
- cumulativeAmount: finalCumulativeAmount.toString(),
305
- expiresAt: this.getSessionExpiresAt(),
306
- meter: 'close',
307
- payer: this.wallet.address,
308
- recipient: input.recipient,
309
- sequence: input.sequence,
310
- serverNonce: input.serverNonce,
311
- units: '0',
312
- });
313
-
314
- const closeTx = await this.resolveCloseTx(input);
315
-
316
- this.channels.set(input.channelId, {
317
- deposited: progress?.deposited ?? 0n,
318
- lastCumulative: finalCumulativeAmount,
319
- lastSequence: input.sequence,
320
- signerAddress: sessionSigner.address,
321
- ...(progress?.swigRoleId !== undefined
322
- ? { swigRoleId: progress.swigRoleId }
323
- : this.sessionRoleId !== null
324
- ? { swigRoleId: this.sessionRoleId }
325
- : {}),
326
- });
327
-
328
- return {
329
- voucher,
330
- ...(closeTx ? { closeTx } : {}),
331
- };
332
- }
333
-
334
- private async signSwigVoucher(signer: KeyPairSigner, voucher: SessionVoucher): Promise<SignedSessionVoucher> {
335
- const signed = await signVoucher(signer, voucher);
336
- return {
337
- ...signed,
338
- signatureType: 'swig-session',
339
- };
340
- }
341
-
342
- private assertProgramAllowed(channelProgram: string) {
343
- if (!this.allowedPrograms) {
344
- return;
345
- }
346
-
347
- if (!this.allowedPrograms.has(channelProgram)) {
348
- throw new Error(`Channel program is not allowed: ${channelProgram}`);
349
- }
350
- }
351
-
352
- private assertMonotonic(
353
- channelId: string,
354
- sequence: number,
355
- cumulativeAmount: bigint,
356
- progress: ChannelProgress | undefined,
357
- ) {
358
- if (!Number.isInteger(sequence) || sequence < 0) {
359
- throw new Error('Sequence must be a non-negative integer');
360
- }
361
-
362
- if (!progress) {
363
- return;
364
- }
365
-
366
- if (sequence <= progress.lastSequence) {
367
- throw new Error(
368
- `Sequence must increase for channel ${channelId}. Last=${progress.lastSequence}, received=${sequence}`,
369
- );
370
- }
371
-
372
- if (cumulativeAmount < progress.lastCumulative) {
373
- throw new Error(
374
- `Cumulative amount must not decrease for channel ${channelId}. Last=${progress.lastCumulative.toString()}, received=${cumulativeAmount.toString()}`,
375
- );
376
- }
377
- }
378
-
379
- private requireActiveSessionSigner(channelId: string, progress: ChannelProgress | undefined): KeyPairSigner {
380
- if (!this.sessionSigner || this.sessionStartedAtMs === null) {
381
- throw new Error(`No active Swig session key for channel ${channelId}. Call authorizeOpen first.`);
382
- }
383
-
384
- if (this.isSessionExpired()) {
385
- throw new Error('Swig session key has expired. Re-open the channel to create a fresh session key.');
386
- }
387
-
388
- if (progress && progress.signerAddress !== this.sessionSigner.address) {
389
- throw new Error(
390
- `Session signer changed for channel ${channelId}; expected ${progress.signerAddress}, active ${this.sessionSigner.address}`,
391
- );
392
- }
393
-
394
- if (
395
- progress?.swigRoleId !== undefined &&
396
- this.sessionRoleId !== null &&
397
- progress.swigRoleId !== this.sessionRoleId
398
- ) {
399
- throw new Error(
400
- `Swig role changed for channel ${channelId}; expected ${progress.swigRoleId}, active ${this.sessionRoleId}`,
401
- );
402
- }
403
-
404
- return this.sessionSigner;
405
- }
406
-
407
- private async ensureSessionSignerForOpen(input: AuthorizeOpenInput): Promise<SessionSignerState> {
408
- if (this.sessionSigner && !this.isSessionExpired()) {
409
- return {
410
- signer: this.sessionSigner,
411
- ...(this.sessionOpenTx ? { openTx: this.sessionOpenTx } : {}),
412
- ...(this.sessionRoleId !== null ? { swigRoleId: this.sessionRoleId } : {}),
413
- ...(this.sessionStartedAtMs !== null ? { createdAtMs: this.sessionStartedAtMs } : {}),
414
- };
415
- }
416
-
417
- const existingResult = this.wallet.getSessionKey ? await this.wallet.getSessionKey() : null;
418
- if (existingResult) {
419
- const existing = normalizeSessionSignerState(existingResult, 'getSessionKey');
420
-
421
- // Reuse only when wallet can prove when this session actually started.
422
- if (existing.createdAtMs !== undefined) {
423
- this.setSessionState(existing);
424
- return existing;
425
- }
426
-
427
- if (!this.wallet.createSessionKey) {
428
- throw new Error(
429
- 'Swig wallet getSessionKey() must include `createdAt` when createSessionKey() is unavailable, so session TTL can be validated safely',
430
- );
431
- }
432
- }
433
-
434
- if (!this.wallet.createSessionKey) {
435
- throw new Error(
436
- 'Swig wallet must implement createSessionKey() or getSessionKey() to use SwigSessionAuthorizer',
437
- );
438
- }
439
-
440
- const createdResult = await this.wallet.createSessionKey({
441
- ttlSeconds: this.policy.ttlSeconds,
442
- ...(this.policy.spendLimit ? { spendLimit: this.policy.spendLimit } : {}),
443
- ...(this.policy.depositLimit ? { depositLimit: this.policy.depositLimit } : {}),
444
- channelId: input.channelId,
445
- channelProgram: input.channelProgram,
446
- network: input.network,
447
- recipient: input.recipient,
448
- });
449
-
450
- const created = normalizeSessionSignerState(createdResult, 'createSessionKey');
451
- const resolvedCreated: SessionSignerState = {
452
- ...created,
453
- createdAtMs: created.createdAtMs ?? Date.now(),
454
- };
455
-
456
- this.setSessionState(resolvedCreated);
457
- return resolvedCreated;
458
- }
459
-
460
- private async ensureSwigInstalled() {
461
- if (this.swigLoaded) {
462
- return;
463
- }
464
-
465
- try {
466
- const swigPackageName = '@swig-wallet/kit';
467
- const module = (await import(swigPackageName)) as Partial<SwigSessionModule>;
468
- if (typeof module.fetchSwig !== 'function') {
469
- throw new Error('Installed `@swig-wallet/kit` does not export fetchSwig() at runtime');
470
- }
471
- this.swigModule = {
472
- fetchSwig: module.fetchSwig,
473
- };
474
- this.swigLoaded = true;
475
- } catch {
476
- throw new Error(
477
- 'SwigSessionAuthorizer requires the optional dependency `@swig-wallet/kit`. Install it with `npm install @swig-wallet/kit` to use `swig_session` mode.',
478
- );
479
- }
480
- }
481
-
482
- private isSessionExpired(): boolean {
483
- if (this.sessionStartedAtMs === null) {
484
- return true;
485
- }
486
-
487
- return Date.now() > this.sessionStartedAtMs + this.policy.ttlSeconds * 1000;
488
- }
489
-
490
- private getSessionExpiresAt(): string {
491
- const start = this.sessionStartedAtMs ?? Date.now();
492
- return new Date(start + this.policy.ttlSeconds * 1000).toISOString();
493
- }
494
-
495
- private async resolveOpenTx(input: AuthorizeOpenInput, session: SessionSignerState): Promise<string> {
496
- if (!this.buildOpenTx) {
497
- if (!session.openTx) {
498
- throw new Error(
499
- 'SwigSessionAuthorizer requires `buildOpenTx` or a session setup result that includes `openTx` from `createSessionKey()`/`getSessionKey()`',
500
- );
501
- }
502
-
503
- return session.openTx;
504
- }
505
-
506
- return await this.buildOpenTx(input);
507
- }
508
-
509
- private async resolveTopupTx(input: AuthorizeTopupInput): Promise<string> {
510
- if (!this.buildTopupTx) {
511
- throw new Error('SwigSessionAuthorizer requires `buildTopupTx` to authorize topup requests');
512
- }
513
-
514
- return await this.buildTopupTx(input);
515
- }
516
-
517
- private async resolveCloseTx(input: AuthorizeCloseInput): Promise<string | undefined> {
518
- if (!this.buildCloseTx) {
519
- return undefined;
520
- }
521
-
522
- return await this.buildCloseTx(input);
523
- }
524
-
525
- private setSessionState(state: SessionSignerState) {
526
- this.sessionSigner = state.signer;
527
- this.sessionStartedAtMs = state.createdAtMs ?? Date.now();
528
- this.sessionOpenTx = state.openTx ?? null;
529
- this.sessionRoleId = state.swigRoleId ?? this.wallet.swigRoleId ?? null;
530
- this.validatedPolicyForSessionSigner = null;
531
- }
532
-
533
- private resolveRpcUrl(network: string): string {
534
- return this.rpcUrl ?? DEFAULT_RPC_URLS[network] ?? DEFAULT_RPC_URLS['mainnet-beta'];
535
- }
536
-
537
- private async assertPolicyAppliedOnChain(input: AuthorizeOpenInput, session: SessionSignerState) {
538
- // Cache by delegated signer to avoid repeating RPC lookups on every open.
539
- if (this.validatedPolicyForSessionSigner === session.signer.address) {
540
- return;
541
- }
542
-
543
- if (!this.wallet.swigAddress) {
544
- throw new Error(
545
- 'Swig wallet adapter must provide `swigAddress` to validate on-chain session policy limits',
546
- );
547
- }
548
-
549
- const swigModule = this.swigModule;
550
- if (!swigModule) {
551
- throw new Error('Swig SDK was not loaded before on-chain validation');
552
- }
553
-
554
- const rpcUrl = this.resolveRpcUrl(input.network);
555
- const rpc = createSolanaRpc(rpcUrl);
556
- const swig = await swigModule.fetchSwig(rpc, this.wallet.swigAddress);
557
-
558
- const role = this.resolveSessionRole(swig, session);
559
- const actions = role.actions;
560
-
561
- if (!actions) {
562
- throw new Error(`Swig role ${role.id} does not expose action metadata for policy validation`);
563
- }
564
-
565
- this.assertRoleAllowsProgram(actions, input.channelProgram, role.id);
566
-
567
- const onChainSpendLimit = this.resolveOnChainSpendLimit(actions, input);
568
- this.assertLimitAtMostPolicy(onChainSpendLimit, this.spendLimit, 'spendLimit', role.id, input.asset);
569
- this.assertLimitAtMostPolicy(onChainSpendLimit, this.depositLimit, 'depositLimit', role.id, input.asset);
570
-
571
- this.sessionRoleId = role.id;
572
- this.validatedPolicyForSessionSigner = session.signer.address;
573
- }
574
-
575
- private resolveSessionRole(swig: SwigAccount, session: SessionSignerState): SwigRole {
576
- // Prefer explicit role binding, then validate it against session key lookup.
577
- const preferredRoleId = session.swigRoleId ?? this.sessionRoleId ?? this.wallet.swigRoleId;
578
-
579
- if (preferredRoleId !== undefined && preferredRoleId !== null && swig.findRoleById) {
580
- const roleById = swig.findRoleById(preferredRoleId);
581
- if (roleById) {
582
- if (swig.findRoleBySessionKey) {
583
- const roleBySessionKey = swig.findRoleBySessionKey(session.signer.address);
584
- if (!roleBySessionKey) {
585
- throw new Error(
586
- `Unable to locate a Swig role for delegated session key ${session.signer.address}`,
587
- );
588
- }
589
-
590
- if (roleBySessionKey.id !== roleById.id) {
591
- throw new Error(
592
- `Swig role ${preferredRoleId} does not match delegated session key role ${roleBySessionKey.id}`,
593
- );
594
- }
595
- }
596
-
597
- return roleById;
598
- }
599
-
600
- throw new Error(`Unable to locate Swig role ${preferredRoleId} for session key ${session.signer.address}`);
601
- }
602
-
603
- if (!swig.findRoleBySessionKey) {
604
- throw new Error(
605
- 'Swig account object does not expose findRoleBySessionKey() required for policy validation',
606
- );
607
- }
608
-
609
- const roleBySessionKey = swig.findRoleBySessionKey(session.signer.address);
610
- if (!roleBySessionKey) {
611
- throw new Error(`Unable to locate a Swig role for delegated session key ${session.signer.address}`);
612
- }
613
-
614
- return roleBySessionKey;
615
- }
616
-
617
- private assertRoleAllowsProgram(actions: SwigRoleActions, channelProgram: string, roleId: number) {
618
- if (!actions.canUseProgram) {
619
- return;
620
- }
621
-
622
- if (!actions.canUseProgram(channelProgram)) {
623
- throw new Error(`Swig role ${roleId} does not allow channel program ${channelProgram}`);
624
- }
625
- }
626
-
627
- private resolveOnChainSpendLimit(actions: SwigRoleActions, input: AuthorizeOpenInput): bigint | null {
628
- if (input.asset.kind === 'spl') {
629
- if (!input.asset.mint) {
630
- throw new Error('asset.mint is required for SPL session policy validation');
631
- }
632
-
633
- if (!actions.tokenSpendLimit) {
634
- throw new Error('Swig role does not expose tokenSpendLimit() for SPL policy validation');
635
- }
636
-
637
- return actions.tokenSpendLimit(input.asset.mint);
638
- }
639
-
640
- if (!actions.solSpendLimit) {
641
- throw new Error('Swig role does not expose solSpendLimit() for SOL policy validation');
642
- }
643
-
644
- return actions.solSpendLimit();
645
- }
646
-
647
- private assertLimitAtMostPolicy(
648
- onChainLimit: bigint | null,
649
- policyLimit: bigint | undefined,
650
- field: 'depositLimit' | 'spendLimit',
651
- roleId: number,
652
- asset: AuthorizeOpenInput['asset'],
653
- ) {
654
- if (policyLimit === undefined) {
655
- return;
656
- }
657
-
658
- if (onChainLimit === null) {
659
- throw new Error(
660
- `Swig role ${roleId} has uncapped ${asset.kind.toUpperCase()} spending, but policy ${field}=${policyLimit.toString()} requires an on-chain cap`,
661
- );
662
- }
663
-
664
- if (onChainLimit > policyLimit) {
665
- throw new Error(
666
- `Swig role ${roleId} on-chain limit ${onChainLimit.toString()} exceeds policy ${field}=${policyLimit.toString()}`,
667
- );
668
- }
669
- }
670
- }
671
-
672
- function normalizeSessionSignerState(
673
- value: SwigSessionKeyResult,
674
- source: 'createSessionKey' | 'getSessionKey',
675
- ): SessionSignerState {
676
- // Backward compatibility: some wallet adapters return a signer directly.
677
- if (isSignerLike(value)) {
678
- return { signer: value };
679
- }
680
-
681
- if (!value || typeof value !== 'object') {
682
- throw new Error(`Swig wallet ${source}() must return a signer or an object containing { signer }`);
683
- }
684
-
685
- const signer = (value as { signer?: unknown }).signer;
686
- if (!isSignerLike(signer)) {
687
- throw new Error(`Swig wallet ${source}() returned an invalid signer`);
688
- }
689
-
690
- const openTx = (value as { openTx?: unknown }).openTx;
691
- if (openTx !== undefined && typeof openTx !== 'string') {
692
- throw new Error(`Swig wallet ${source}() returned invalid openTx; expected string`);
693
- }
694
-
695
- const swigRoleIdRaw = (value as { swigRoleId?: unknown }).swigRoleId;
696
- let swigRoleId: number | undefined;
697
- if (swigRoleIdRaw !== undefined) {
698
- if (typeof swigRoleIdRaw !== 'number') {
699
- throw new Error(`Swig wallet ${source}() returned invalid swigRoleId; expected a non-negative integer`);
700
- }
701
-
702
- if (!Number.isInteger(swigRoleIdRaw) || swigRoleIdRaw < 0) {
703
- throw new Error(`Swig wallet ${source}() returned invalid swigRoleId; expected a non-negative integer`);
704
- }
705
-
706
- swigRoleId = swigRoleIdRaw;
707
- }
708
-
709
- const createdAtRaw = (value as { createdAt?: unknown }).createdAt;
710
- let createdAtMs: number | undefined;
711
- if (createdAtRaw !== undefined) {
712
- createdAtMs = parseCreatedAt(createdAtRaw, source);
713
- }
714
-
715
- return {
716
- signer,
717
- ...(openTx !== undefined && openTx.trim().length > 0 ? { openTx } : {}),
718
- ...(swigRoleId !== undefined ? { swigRoleId } : {}),
719
- ...(createdAtMs !== undefined ? { createdAtMs } : {}),
720
- };
721
- }
722
-
723
- function isSignerLike(value: unknown): value is KeyPairSigner {
724
- return !!value && typeof value === 'object' && typeof (value as { address?: unknown }).address === 'string';
725
- }
726
-
727
- function parseCreatedAt(value: unknown, source: 'createSessionKey' | 'getSessionKey'): number {
728
- const unixMs =
729
- value instanceof Date
730
- ? value.getTime()
731
- : typeof value === 'number'
732
- ? value
733
- : typeof value === 'string'
734
- ? Date.parse(value)
735
- : Number.NaN;
736
-
737
- if (!Number.isFinite(unixMs) || unixMs <= 0) {
738
- throw new Error(
739
- `Swig wallet ${source}() returned invalid createdAt; expected Date, unix milliseconds, or ISO timestamp string`,
740
- );
741
- }
742
-
743
- return unixMs;
744
- }
745
-
746
- function parseNonNegativeAmount(value: string, field: string): bigint {
747
- let amount: bigint;
748
- try {
749
- amount = BigInt(value);
750
- } catch {
751
- throw new Error(`${field} must be a valid integer string`);
752
- }
753
-
754
- if (amount < 0n) {
755
- throw new Error(`${field} must be non-negative`);
756
- }
757
-
758
- return amount;
759
- }
760
-
761
- function normalizeChainId(network: string): string {
762
- const normalized = network.trim();
763
- if (normalized.length === 0) {
764
- throw new Error('network must be a non-empty string');
765
- }
766
- return normalized.startsWith('solana:') ? normalized : `solana:${normalized}`;
767
- }