zksync-sso 0.2.0 → 0.3.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.
- package/README.md +11 -2
- package/dist/_cjs/client/session/actions/session.js +53 -1
- package/dist/_cjs/client/session/actions/session.js.map +1 -1
- package/dist/_cjs/client/session/client.js +19 -0
- package/dist/_cjs/client/session/client.js.map +1 -1
- package/dist/_cjs/client/session/decorators/wallet.js +39 -0
- package/dist/_cjs/client/session/decorators/wallet.js.map +1 -1
- package/dist/_cjs/client-auth-server/Signer.js +25 -1
- package/dist/_cjs/client-auth-server/Signer.js.map +1 -1
- package/dist/_cjs/client-auth-server/WalletProvider.js +3 -1
- package/dist/_cjs/client-auth-server/WalletProvider.js.map +1 -1
- package/dist/_cjs/utils/helpers.js +26 -0
- package/dist/_cjs/utils/helpers.js.map +1 -1
- package/dist/_cjs/utils/session.js +283 -1
- package/dist/_cjs/utils/session.js.map +1 -1
- package/dist/_esm/client/session/actions/session.js +59 -1
- package/dist/_esm/client/session/actions/session.js.map +1 -1
- package/dist/_esm/client/session/client.js +20 -0
- package/dist/_esm/client/session/client.js.map +1 -1
- package/dist/_esm/client/session/decorators/wallet.js +47 -0
- package/dist/_esm/client/session/decorators/wallet.js.map +1 -1
- package/dist/_esm/client-auth-server/Signer.js +25 -1
- package/dist/_esm/client-auth-server/Signer.js.map +1 -1
- package/dist/_esm/client-auth-server/WalletProvider.js +3 -1
- package/dist/_esm/client-auth-server/WalletProvider.js.map +1 -1
- package/dist/_esm/utils/helpers.js +24 -0
- package/dist/_esm/utils/helpers.js.map +1 -1
- package/dist/_esm/utils/session.js +296 -1
- package/dist/_esm/utils/session.js.map +1 -1
- package/dist/_types/client/session/actions/session.d.ts +26 -1
- package/dist/_types/client/session/actions/session.d.ts.map +1 -1
- package/dist/_types/client/session/client.d.ts +6 -1
- package/dist/_types/client/session/client.d.ts.map +1 -1
- package/dist/_types/client/session/decorators/wallet.d.ts.map +1 -1
- package/dist/_types/client-auth-server/Signer.d.ts +10 -1
- package/dist/_types/client-auth-server/Signer.d.ts.map +1 -1
- package/dist/_types/client-auth-server/WalletProvider.d.ts +8 -1
- package/dist/_types/client-auth-server/WalletProvider.d.ts.map +1 -1
- package/dist/_types/utils/helpers.d.ts +7 -0
- package/dist/_types/utils/helpers.d.ts.map +1 -1
- package/dist/_types/utils/session.d.ts +51 -0
- package/dist/_types/utils/session.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/session/actions/session.ts +85 -2
- package/src/client/session/client.ts +28 -1
- package/src/client/session/decorators/wallet.ts +59 -3
- package/src/client-auth-server/Signer.ts +17 -1
- package/src/client-auth-server/WalletProvider.ts +6 -1
- package/src/utils/helpers.ts +28 -0
- package/src/utils/session.ts +356 -1
package/src/utils/session.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { type Address, getAddress, type Hash, type Hex } from "viem";
|
|
1
|
+
import { type Address, bytesToHex, getAddress, type Hash, type Hex, hexToBigInt, hexToBytes } from "viem";
|
|
2
|
+
|
|
3
|
+
import { calculateMaxFee, findSmallestBigInt } from "./helpers.js";
|
|
2
4
|
|
|
3
5
|
export enum LimitType {
|
|
4
6
|
Unlimited = 0,
|
|
@@ -167,3 +169,356 @@ export const getPeriodIdsForTransaction = (args: {
|
|
|
167
169
|
];
|
|
168
170
|
return periodIds;
|
|
169
171
|
};
|
|
172
|
+
|
|
173
|
+
export enum SessionErrorType {
|
|
174
|
+
SessionInactive = "session_inactive",
|
|
175
|
+
SessionExpired = "session_expired",
|
|
176
|
+
FeeLimitExceeded = "fee_limit_exceeded",
|
|
177
|
+
NoCallPolicy = "no_call_policy",
|
|
178
|
+
NoTransferPolicy = "no_transfer_policy",
|
|
179
|
+
MaxValuePerUseExceeded = "max_value_per_use_exceeded",
|
|
180
|
+
ValueLimitExceeded = "value_limit_exceeded",
|
|
181
|
+
ConstraintIndexOutOfBounds = "constraint_index_out_of_bounds",
|
|
182
|
+
NoLimitStateFound = "no_limit_state_found",
|
|
183
|
+
ParameterLimitExceeded = "parameter_limit_exceeded",
|
|
184
|
+
ConstraintEqualViolated = "constraint_equal_violated",
|
|
185
|
+
ConstraintGreaterViolated = "constraint_greater_violated",
|
|
186
|
+
ConstraintLessViolated = "constraint_less_violated",
|
|
187
|
+
ConstraintGreaterEqualViolated = "constraint_greater_equal_violated",
|
|
188
|
+
ConstraintLessEqualViolated = "constraint_less_equal_violated",
|
|
189
|
+
ConstraintNotEqualViolated = "constraint_not_equal_violated",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export enum SessionEventType {
|
|
193
|
+
Expired = "session_expired",
|
|
194
|
+
Revoked = "session_revoked",
|
|
195
|
+
Inactive = "session_inactive",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type SessionStateEvent = {
|
|
199
|
+
type: SessionEventType;
|
|
200
|
+
message: string;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export type SessionStateEventCallback = (event: SessionStateEvent) => void;
|
|
204
|
+
|
|
205
|
+
export type ValidationResult = {
|
|
206
|
+
valid: boolean;
|
|
207
|
+
error: null | {
|
|
208
|
+
type: SessionErrorType;
|
|
209
|
+
message: string;
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export type TransactionValidationArgs = {
|
|
214
|
+
sessionState: SessionState;
|
|
215
|
+
sessionConfig: SessionConfig;
|
|
216
|
+
transaction: {
|
|
217
|
+
to: Address;
|
|
218
|
+
value?: bigint;
|
|
219
|
+
data?: Hex;
|
|
220
|
+
gas?: bigint;
|
|
221
|
+
gasPrice?: bigint;
|
|
222
|
+
maxFeePerGas?: bigint;
|
|
223
|
+
maxPriorityFeePerGas?: bigint;
|
|
224
|
+
paymaster?: Address;
|
|
225
|
+
};
|
|
226
|
+
currentTimestamp?: bigint;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export function validateSessionTransaction(args: TransactionValidationArgs): ValidationResult {
|
|
230
|
+
const { sessionState, sessionConfig, transaction, currentTimestamp } = args;
|
|
231
|
+
const timestamp = currentTimestamp || BigInt(Math.floor(Date.now() / 1000));
|
|
232
|
+
|
|
233
|
+
// 1. Check if session is active
|
|
234
|
+
if (sessionState.status !== SessionStatus.Active) {
|
|
235
|
+
return {
|
|
236
|
+
valid: false,
|
|
237
|
+
error: {
|
|
238
|
+
type: SessionErrorType.SessionInactive,
|
|
239
|
+
message: "Session is not active",
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 2. Check if session hasn't expired
|
|
245
|
+
if (sessionConfig.expiresAt <= timestamp) {
|
|
246
|
+
return {
|
|
247
|
+
valid: false,
|
|
248
|
+
error: {
|
|
249
|
+
type: SessionErrorType.SessionExpired,
|
|
250
|
+
message: "Session has expired",
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 3. Extract transaction data
|
|
256
|
+
const to = getAddress(transaction.to.toLowerCase());
|
|
257
|
+
const value = transaction.value || 0n;
|
|
258
|
+
const data = transaction.data || "0x";
|
|
259
|
+
const gas = transaction.gas || 0n;
|
|
260
|
+
const selector = data.length >= 10 ? data.slice(0, 10) as Hash : undefined;
|
|
261
|
+
|
|
262
|
+
if (!transaction.paymaster) {
|
|
263
|
+
const maxFee = calculateMaxFee({
|
|
264
|
+
gas,
|
|
265
|
+
gasPrice: transaction.gasPrice,
|
|
266
|
+
maxFeePerGas: transaction.maxFeePerGas,
|
|
267
|
+
maxPriorityFeePerGas: transaction.maxPriorityFeePerGas,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (maxFee > sessionState.feesRemaining) {
|
|
271
|
+
return {
|
|
272
|
+
valid: false,
|
|
273
|
+
error: {
|
|
274
|
+
type: SessionErrorType.FeeLimitExceeded,
|
|
275
|
+
message: `Transaction max fee ${maxFee} exceeds remaining fee limit ${sessionState.feesRemaining}`,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const isContractCall = !!selector;
|
|
282
|
+
if (isContractCall) {
|
|
283
|
+
const policies = sessionConfig.callPolicies.filter(
|
|
284
|
+
(policy) => policy.target === to && policy.selector === selector,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (!policies.length) {
|
|
288
|
+
return {
|
|
289
|
+
valid: false,
|
|
290
|
+
error: {
|
|
291
|
+
type: SessionErrorType.NoCallPolicy,
|
|
292
|
+
message: `No call policy found for target ${to} with selector ${selector}`,
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Verify max value per use
|
|
298
|
+
const lowestMaxValuePerUse = findSmallestBigInt(policies.map((policy) => policy.maxValuePerUse));
|
|
299
|
+
if (value > lowestMaxValuePerUse) {
|
|
300
|
+
return {
|
|
301
|
+
valid: false,
|
|
302
|
+
error: {
|
|
303
|
+
type: SessionErrorType.MaxValuePerUseExceeded,
|
|
304
|
+
message: `Transaction value ${value} exceeds max value per use ${lowestMaxValuePerUse}`,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Verify remaining value limit
|
|
310
|
+
const remainingValue = findLowestRemainingValue(sessionState.callValue, to, selector);
|
|
311
|
+
if (value > remainingValue) {
|
|
312
|
+
return {
|
|
313
|
+
valid: false,
|
|
314
|
+
error: {
|
|
315
|
+
type: SessionErrorType.ValueLimitExceeded,
|
|
316
|
+
message: `Transaction value ${value} exceeds remaining value limit ${remainingValue || 0n}`,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for (const policy of policies) {
|
|
322
|
+
const constraintResult = validateConstraints(data, policy.constraints, sessionState.callParams);
|
|
323
|
+
if (!constraintResult.valid) return constraintResult;
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
// This is a simple transfer
|
|
327
|
+
const policies = sessionConfig.transferPolicies.filter((policy) => policy.target === to);
|
|
328
|
+
if (!policies.length) {
|
|
329
|
+
return {
|
|
330
|
+
valid: false,
|
|
331
|
+
error: {
|
|
332
|
+
type: SessionErrorType.NoTransferPolicy,
|
|
333
|
+
message: `No transfer policy found for target ${to}`,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Verify max value per use
|
|
339
|
+
const lowestMaxValuePerUse = findSmallestBigInt(policies.map((policy) => policy.maxValuePerUse));
|
|
340
|
+
if (value > lowestMaxValuePerUse) {
|
|
341
|
+
return {
|
|
342
|
+
valid: false,
|
|
343
|
+
error: {
|
|
344
|
+
type: SessionErrorType.MaxValuePerUseExceeded,
|
|
345
|
+
message: `Transaction value ${value} exceeds max value per use ${lowestMaxValuePerUse}`,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Verify remaining value limit
|
|
351
|
+
const remainingValue = findLowestRemainingValue(sessionState.transferValue, to);
|
|
352
|
+
if (value > remainingValue) {
|
|
353
|
+
return {
|
|
354
|
+
valid: false,
|
|
355
|
+
error: {
|
|
356
|
+
type: SessionErrorType.ValueLimitExceeded,
|
|
357
|
+
message: `Transaction value ${value} exceeds remaining value limit ${remainingValue || 0n}`,
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
valid: true,
|
|
365
|
+
error: null,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function findLowestRemainingValue(
|
|
370
|
+
valueArray: SessionState["transferValue"] | SessionState["callValue"],
|
|
371
|
+
target: Address,
|
|
372
|
+
selector?: Hash,
|
|
373
|
+
): bigint {
|
|
374
|
+
if (selector) {
|
|
375
|
+
const filtered = valueArray
|
|
376
|
+
.filter((item) => item.target === target && item.selector === selector)
|
|
377
|
+
.map((item) => item.remaining);
|
|
378
|
+
if (!filtered.length) return 0n;
|
|
379
|
+
return findSmallestBigInt(filtered);
|
|
380
|
+
} else {
|
|
381
|
+
const filtered = valueArray
|
|
382
|
+
.filter((item) => item.target === target)
|
|
383
|
+
.map((item) => item.remaining);
|
|
384
|
+
if (!filtered.length) return 0n;
|
|
385
|
+
return findSmallestBigInt(filtered);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function validateConstraints(
|
|
390
|
+
data: Hex,
|
|
391
|
+
constraints: Constraint[],
|
|
392
|
+
callParams: SessionState["callParams"],
|
|
393
|
+
): ValidationResult {
|
|
394
|
+
const dataBytes = hexToBytes(data);
|
|
395
|
+
|
|
396
|
+
for (const constraint of constraints) {
|
|
397
|
+
// Find proper index and extract data
|
|
398
|
+
const index = Number(constraint.index);
|
|
399
|
+
if (index + 32 > dataBytes.length) {
|
|
400
|
+
return {
|
|
401
|
+
valid: false,
|
|
402
|
+
error: {
|
|
403
|
+
type: SessionErrorType.ConstraintIndexOutOfBounds,
|
|
404
|
+
message: `Constraint index ${index} out of bounds for data length ${dataBytes.length}`,
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const parameterBytes = dataBytes.slice(index, index + 32);
|
|
410
|
+
const parameterValue = bytesToHex(parameterBytes);
|
|
411
|
+
const refValue = constraint.refValue;
|
|
412
|
+
|
|
413
|
+
// Find remaining limit value if applicable
|
|
414
|
+
let remaining: bigint | undefined;
|
|
415
|
+
if (constraint.limit.limitType !== LimitType.Unlimited) {
|
|
416
|
+
const paramItem = callParams.find((item) => Number(item.index) === index);
|
|
417
|
+
remaining = paramItem?.remaining;
|
|
418
|
+
|
|
419
|
+
// If this is a limited constraint, but we don't have state for it, something is wrong
|
|
420
|
+
if (remaining === undefined) {
|
|
421
|
+
return {
|
|
422
|
+
valid: false,
|
|
423
|
+
error: {
|
|
424
|
+
type: SessionErrorType.NoLimitStateFound,
|
|
425
|
+
message: `No remaining limit state found for constraint at index ${index}`,
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Check if parameter value exceeds remaining limit
|
|
431
|
+
const parameterValueBigInt = hexToBigInt(parameterValue);
|
|
432
|
+
if (parameterValueBigInt > remaining) {
|
|
433
|
+
return {
|
|
434
|
+
valid: false,
|
|
435
|
+
error: {
|
|
436
|
+
type: SessionErrorType.ParameterLimitExceeded,
|
|
437
|
+
message: `Parameter value ${parameterValueBigInt} at index ${index} exceeds remaining limit ${remaining}`,
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Check condition
|
|
444
|
+
switch (constraint.condition) {
|
|
445
|
+
case ConstraintCondition.Equal:
|
|
446
|
+
if (parameterValue !== refValue) {
|
|
447
|
+
return {
|
|
448
|
+
valid: false,
|
|
449
|
+
error: {
|
|
450
|
+
type: SessionErrorType.ConstraintEqualViolated,
|
|
451
|
+
message: `Parameter value ${parameterValue} must equal ${refValue}`,
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
case ConstraintCondition.Greater:
|
|
457
|
+
if (parameterValue <= refValue) {
|
|
458
|
+
return {
|
|
459
|
+
valid: false,
|
|
460
|
+
error: {
|
|
461
|
+
type: SessionErrorType.ConstraintGreaterViolated,
|
|
462
|
+
message: `Parameter value ${parameterValue} must be greater than ${refValue}`,
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
break;
|
|
467
|
+
case ConstraintCondition.Less:
|
|
468
|
+
if (parameterValue >= refValue) {
|
|
469
|
+
return {
|
|
470
|
+
valid: false,
|
|
471
|
+
error: {
|
|
472
|
+
type: SessionErrorType.ConstraintLessViolated,
|
|
473
|
+
message: `Parameter value ${parameterValue} must be less than ${refValue}`,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
case ConstraintCondition.GreaterEqual:
|
|
479
|
+
if (parameterValue < refValue) {
|
|
480
|
+
return {
|
|
481
|
+
valid: false,
|
|
482
|
+
error: {
|
|
483
|
+
type: SessionErrorType.ConstraintGreaterEqualViolated,
|
|
484
|
+
message: `Parameter value ${parameterValue} must be greater than or equal to ${refValue}`,
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
case ConstraintCondition.LessEqual:
|
|
490
|
+
if (parameterValue > refValue) {
|
|
491
|
+
return {
|
|
492
|
+
valid: false,
|
|
493
|
+
error: {
|
|
494
|
+
type: SessionErrorType.ConstraintLessEqualViolated,
|
|
495
|
+
message: `Parameter value ${parameterValue} must be less than or equal to ${refValue}`,
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
case ConstraintCondition.NotEqual:
|
|
501
|
+
if (parameterValue === refValue) {
|
|
502
|
+
return {
|
|
503
|
+
valid: false,
|
|
504
|
+
error: {
|
|
505
|
+
type: SessionErrorType.ConstraintNotEqualViolated,
|
|
506
|
+
message: `Parameter value ${parameterValue} must not equal ${refValue}`,
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
break;
|
|
511
|
+
case ConstraintCondition.Unconstrained:
|
|
512
|
+
// Unconstrained means no checks, so we skip
|
|
513
|
+
break;
|
|
514
|
+
default:
|
|
515
|
+
console.warn(`Unhandled constraint condition: ${constraint.condition}`);
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
valid: true,
|
|
522
|
+
error: null,
|
|
523
|
+
};
|
|
524
|
+
}
|