@owney/sdk 0.3.3 → 0.3.4

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 CHANGED
@@ -749,3 +749,7 @@ The three convenience subclasses extend `OwneyError` and remain backwards-compat
749
749
  | `NotConnectedError` | `NOT_CONNECTED` | — |
750
750
  | `AgentNotFoundError` | `AGENT_NOT_FOUND` | — |
751
751
  | `AgentChainIncompatibleError` | `AGENT_CHAIN_INCOMPATIBLE` | `incompatibleAgents`, `connectedChainId` |
752
+
753
+ ## License
754
+
755
+ ELv2
package/dist/index.cjs CHANGED
@@ -333,6 +333,72 @@ function mapApyByStrategy(raw) {
333
333
  };
334
334
  }
335
335
 
336
+ // src/agents/zyfai/zyfai.auth-cache.ts
337
+ var KEY_PREFIX = "owney.zyfai.session";
338
+ var storage = () => {
339
+ if (typeof window === "undefined")
340
+ return null;
341
+ try {
342
+ return window.localStorage;
343
+ } catch {
344
+ return null;
345
+ }
346
+ };
347
+ var buildKey = (address, chainId) => `${KEY_PREFIX}:${address.toLowerCase()}:${chainId}`;
348
+ var isJwtExpired = (token) => {
349
+ const parts = token.split(".");
350
+ if (parts.length !== 3)
351
+ return false;
352
+ try {
353
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
354
+ const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
355
+ const decoded = typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("utf8");
356
+ const claims = JSON.parse(decoded);
357
+ if (typeof claims.exp !== "number")
358
+ return false;
359
+ return Date.now() / 1e3 >= claims.exp - 30;
360
+ } catch {
361
+ return false;
362
+ }
363
+ };
364
+ var readSession = (address, chainId) => {
365
+ const store = storage();
366
+ if (!store)
367
+ return null;
368
+ const raw = store.getItem(buildKey(address, chainId));
369
+ if (!raw)
370
+ return null;
371
+ try {
372
+ const parsed = JSON.parse(raw);
373
+ if (!parsed.token || isJwtExpired(parsed.token)) {
374
+ store.removeItem(buildKey(address, chainId));
375
+ return null;
376
+ }
377
+ return parsed;
378
+ } catch {
379
+ store.removeItem(buildKey(address, chainId));
380
+ return null;
381
+ }
382
+ };
383
+ var writeSession = (address, chainId, session) => {
384
+ const store = storage();
385
+ if (!store)
386
+ return;
387
+ try {
388
+ store.setItem(buildKey(address, chainId), JSON.stringify(session));
389
+ } catch {
390
+ }
391
+ };
392
+ var clearSession = (address, chainId) => {
393
+ const store = storage();
394
+ if (!store)
395
+ return;
396
+ try {
397
+ store.removeItem(buildKey(address, chainId));
398
+ } catch {
399
+ }
400
+ };
401
+
336
402
  // src/agents/zyfai/zyfai.agent.ts
337
403
  var DEFAULT_ZYFAI_RPC_URLS = {
338
404
  8453: "https://base-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
@@ -363,6 +429,49 @@ var ZyfaiAgent = class {
363
429
  apiKey,
364
430
  rpcUrls: rpcUrls ?? DEFAULT_ZYFAI_RPC_URLS
365
431
  });
432
+ this.installAuthCache();
433
+ }
434
+ /**
435
+ * Patches `sdk.authenticateUser` so a fresh page can reuse the JWT from a
436
+ * prior session instead of triggering a new SIWE prompt. Runs inside
437
+ * `connectAccount` AFTER wallet setup but BEFORE the real SIWE handshake
438
+ * (Zyfai dist line 1374). At that point `walletClient` + `currentChainId`
439
+ * are populated, so the cache lookup has both keys it needs.
440
+ *
441
+ * No-op on the server (no `window`) — the SDK is constructed during SSR
442
+ * for some apps and the patch only matters in the browser anyway.
443
+ */
444
+ installAuthCache() {
445
+ if (typeof window === "undefined")
446
+ return;
447
+ const internals = this.sdk;
448
+ const original = internals.authenticateUser.bind(internals);
449
+ internals.authenticateUser = async () => {
450
+ if (internals.authenticatedUserId !== null)
451
+ return;
452
+ const address = internals.walletClient?.account?.address;
453
+ const chainId = internals.currentChainId;
454
+ if (address && chainId) {
455
+ const cached = readSession(address, chainId);
456
+ if (cached) {
457
+ internals.httpClient.setAuthToken(cached.token);
458
+ internals.authenticatedUserId = cached.userId;
459
+ internals.hasActiveSessionKey = cached.hasActiveSessionKey;
460
+ return;
461
+ }
462
+ }
463
+ await original();
464
+ const newAddress = internals.walletClient?.account?.address;
465
+ const newChainId = internals.currentChainId;
466
+ const newToken = internals.httpClient.authToken;
467
+ if (newAddress && newChainId && newToken) {
468
+ writeSession(newAddress, newChainId, {
469
+ token: newToken,
470
+ userId: internals.authenticatedUserId,
471
+ hasActiveSessionKey: internals.hasActiveSessionKey
472
+ });
473
+ }
474
+ };
366
475
  }
367
476
  // --- Private helpers ---
368
477
  getAddress() {
@@ -387,7 +496,7 @@ var ZyfaiAgent = class {
387
496
  return;
388
497
  }
389
498
  this.connectAuthPromise = (async () => {
390
- const address = await this.sdk.connectAccount(
499
+ const address = await this.connectAccountWithRetry(
391
500
  state.provider,
392
501
  validChainId
393
502
  );
@@ -400,6 +509,29 @@ var ZyfaiAgent = class {
400
509
  this.connectAuthPromise = null;
401
510
  }
402
511
  }
512
+ /**
513
+ * Wraps `sdk.connectAccount` with a one-shot retry on auth errors. If the
514
+ * cached JWT is rejected by the backend (revoked, signing-key rotated, etc.)
515
+ * we drop the cache and try again — the second attempt has no cache entry,
516
+ * so it falls through to a real SIWE prompt. Zyfai collapses every 4xx into
517
+ * an `"Unauthorized: Invalid API key"` error so we can't distinguish revoked
518
+ * JWT from a misconfigured API key; we retry once either way, and if the
519
+ * second attempt still fails the original error propagates.
520
+ */
521
+ async connectAccountWithRetry(provider, chainId) {
522
+ try {
523
+ return await this.sdk.connectAccount(provider, chainId);
524
+ } catch (error) {
525
+ const message = error instanceof Error ? error.message : String(error);
526
+ if (!message.toLowerCase().includes("unauthorized"))
527
+ throw error;
528
+ const internals = this.sdk;
529
+ const address = internals.walletClient?.account?.address;
530
+ if (address)
531
+ clearSession(address, chainId);
532
+ return await this.sdk.connectAccount(provider, chainId);
533
+ }
534
+ }
403
535
  /** Full connect: SIWE auth + Safe deploy check + session key. */
404
536
  async connectFull(state, chainId) {
405
537
  await this.connectAuth(state, chainId);
@@ -434,6 +566,9 @@ var ZyfaiAgent = class {
434
566
  }
435
567
  // --- IAgent: Connection lifecycle ---
436
568
  async disconnect() {
569
+ if (this.connectedAddress && this.connectedChainId !== null) {
570
+ clearSession(this.connectedAddress, this.connectedChainId);
571
+ }
437
572
  await this.sdk.disconnectAccount();
438
573
  this.connectedAddress = null;
439
574
  this.connectedChainId = null;
package/dist/index.js CHANGED
@@ -303,6 +303,72 @@ function mapApyByStrategy(raw) {
303
303
  };
304
304
  }
305
305
 
306
+ // src/agents/zyfai/zyfai.auth-cache.ts
307
+ var KEY_PREFIX = "owney.zyfai.session";
308
+ var storage = () => {
309
+ if (typeof window === "undefined")
310
+ return null;
311
+ try {
312
+ return window.localStorage;
313
+ } catch {
314
+ return null;
315
+ }
316
+ };
317
+ var buildKey = (address, chainId) => `${KEY_PREFIX}:${address.toLowerCase()}:${chainId}`;
318
+ var isJwtExpired = (token) => {
319
+ const parts = token.split(".");
320
+ if (parts.length !== 3)
321
+ return false;
322
+ try {
323
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
324
+ const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
325
+ const decoded = typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("utf8");
326
+ const claims = JSON.parse(decoded);
327
+ if (typeof claims.exp !== "number")
328
+ return false;
329
+ return Date.now() / 1e3 >= claims.exp - 30;
330
+ } catch {
331
+ return false;
332
+ }
333
+ };
334
+ var readSession = (address, chainId) => {
335
+ const store = storage();
336
+ if (!store)
337
+ return null;
338
+ const raw = store.getItem(buildKey(address, chainId));
339
+ if (!raw)
340
+ return null;
341
+ try {
342
+ const parsed = JSON.parse(raw);
343
+ if (!parsed.token || isJwtExpired(parsed.token)) {
344
+ store.removeItem(buildKey(address, chainId));
345
+ return null;
346
+ }
347
+ return parsed;
348
+ } catch {
349
+ store.removeItem(buildKey(address, chainId));
350
+ return null;
351
+ }
352
+ };
353
+ var writeSession = (address, chainId, session) => {
354
+ const store = storage();
355
+ if (!store)
356
+ return;
357
+ try {
358
+ store.setItem(buildKey(address, chainId), JSON.stringify(session));
359
+ } catch {
360
+ }
361
+ };
362
+ var clearSession = (address, chainId) => {
363
+ const store = storage();
364
+ if (!store)
365
+ return;
366
+ try {
367
+ store.removeItem(buildKey(address, chainId));
368
+ } catch {
369
+ }
370
+ };
371
+
306
372
  // src/agents/zyfai/zyfai.agent.ts
307
373
  var DEFAULT_ZYFAI_RPC_URLS = {
308
374
  8453: "https://base-mainnet.g.alchemy.com/v2/ZWyVU-9XfS3z8Rn-xkq7V",
@@ -333,6 +399,49 @@ var ZyfaiAgent = class {
333
399
  apiKey,
334
400
  rpcUrls: rpcUrls ?? DEFAULT_ZYFAI_RPC_URLS
335
401
  });
402
+ this.installAuthCache();
403
+ }
404
+ /**
405
+ * Patches `sdk.authenticateUser` so a fresh page can reuse the JWT from a
406
+ * prior session instead of triggering a new SIWE prompt. Runs inside
407
+ * `connectAccount` AFTER wallet setup but BEFORE the real SIWE handshake
408
+ * (Zyfai dist line 1374). At that point `walletClient` + `currentChainId`
409
+ * are populated, so the cache lookup has both keys it needs.
410
+ *
411
+ * No-op on the server (no `window`) — the SDK is constructed during SSR
412
+ * for some apps and the patch only matters in the browser anyway.
413
+ */
414
+ installAuthCache() {
415
+ if (typeof window === "undefined")
416
+ return;
417
+ const internals = this.sdk;
418
+ const original = internals.authenticateUser.bind(internals);
419
+ internals.authenticateUser = async () => {
420
+ if (internals.authenticatedUserId !== null)
421
+ return;
422
+ const address = internals.walletClient?.account?.address;
423
+ const chainId = internals.currentChainId;
424
+ if (address && chainId) {
425
+ const cached = readSession(address, chainId);
426
+ if (cached) {
427
+ internals.httpClient.setAuthToken(cached.token);
428
+ internals.authenticatedUserId = cached.userId;
429
+ internals.hasActiveSessionKey = cached.hasActiveSessionKey;
430
+ return;
431
+ }
432
+ }
433
+ await original();
434
+ const newAddress = internals.walletClient?.account?.address;
435
+ const newChainId = internals.currentChainId;
436
+ const newToken = internals.httpClient.authToken;
437
+ if (newAddress && newChainId && newToken) {
438
+ writeSession(newAddress, newChainId, {
439
+ token: newToken,
440
+ userId: internals.authenticatedUserId,
441
+ hasActiveSessionKey: internals.hasActiveSessionKey
442
+ });
443
+ }
444
+ };
336
445
  }
337
446
  // --- Private helpers ---
338
447
  getAddress() {
@@ -357,7 +466,7 @@ var ZyfaiAgent = class {
357
466
  return;
358
467
  }
359
468
  this.connectAuthPromise = (async () => {
360
- const address = await this.sdk.connectAccount(
469
+ const address = await this.connectAccountWithRetry(
361
470
  state.provider,
362
471
  validChainId
363
472
  );
@@ -370,6 +479,29 @@ var ZyfaiAgent = class {
370
479
  this.connectAuthPromise = null;
371
480
  }
372
481
  }
482
+ /**
483
+ * Wraps `sdk.connectAccount` with a one-shot retry on auth errors. If the
484
+ * cached JWT is rejected by the backend (revoked, signing-key rotated, etc.)
485
+ * we drop the cache and try again — the second attempt has no cache entry,
486
+ * so it falls through to a real SIWE prompt. Zyfai collapses every 4xx into
487
+ * an `"Unauthorized: Invalid API key"` error so we can't distinguish revoked
488
+ * JWT from a misconfigured API key; we retry once either way, and if the
489
+ * second attempt still fails the original error propagates.
490
+ */
491
+ async connectAccountWithRetry(provider, chainId) {
492
+ try {
493
+ return await this.sdk.connectAccount(provider, chainId);
494
+ } catch (error) {
495
+ const message = error instanceof Error ? error.message : String(error);
496
+ if (!message.toLowerCase().includes("unauthorized"))
497
+ throw error;
498
+ const internals = this.sdk;
499
+ const address = internals.walletClient?.account?.address;
500
+ if (address)
501
+ clearSession(address, chainId);
502
+ return await this.sdk.connectAccount(provider, chainId);
503
+ }
504
+ }
373
505
  /** Full connect: SIWE auth + Safe deploy check + session key. */
374
506
  async connectFull(state, chainId) {
375
507
  await this.connectAuth(state, chainId);
@@ -404,6 +536,9 @@ var ZyfaiAgent = class {
404
536
  }
405
537
  // --- IAgent: Connection lifecycle ---
406
538
  async disconnect() {
539
+ if (this.connectedAddress && this.connectedChainId !== null) {
540
+ clearSession(this.connectedAddress, this.connectedChainId);
541
+ }
407
542
  await this.sdk.disconnectAccount();
408
543
  this.connectedAddress = null;
409
544
  this.connectedChainId = null;
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@owney/sdk",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "license": "ELv2",
8
9
  "exports": {
9
10
  ".": {
10
11
  "types": "./dist/index.d.ts",