@buildersgarden/siwa 0.0.11 → 0.0.13

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/dist/siwa.js CHANGED
@@ -7,7 +7,6 @@
7
7
  * Dependencies:
8
8
  * npm install viem
9
9
  */
10
- import { verifyMessage, hashMessage, } from 'viem';
11
10
  import * as crypto from 'crypto';
12
11
  import { getAgent, getReputation } from './registry.js';
13
12
  // ─── Types ───────────────────────────────────────────────────────────
@@ -39,11 +38,12 @@ export function buildSIWAResponse(result) {
39
38
  agentRegistry: result.agentRegistry || undefined,
40
39
  chainId: result.chainId || undefined,
41
40
  verified: result.verified,
41
+ ...(result.signerType ? { signerType: result.signerType } : {}),
42
42
  };
43
43
  const skillRef = {
44
44
  name: '@buildersgarden/siwa',
45
45
  install: 'npm install @buildersgarden/siwa',
46
- url: 'https://siwa.builders.garden/skill.md',
46
+ url: 'https://siwa.id/skill.md',
47
47
  };
48
48
  if (result.valid) {
49
49
  return { status: 'authenticated', ...base };
@@ -284,39 +284,55 @@ export async function createSIWANonce(params, client, options) {
284
284
  return result;
285
285
  }
286
286
  /**
287
- * Sign a SIWA message using the secure keystore.
287
+ * Sign a SIWA message using the provided signer.
288
288
  *
289
- * The private key is loaded from the keystore, used to sign, and discarded.
290
- * It is NEVER returned or exposed to the caller.
289
+ * The signer abstracts the wallet implementation, allowing you to use:
290
+ * - createKeyringProxySigner(config) — Keyring proxy server
291
+ * - createLocalAccountSigner(account) — viem LocalAccount (private key)
292
+ * - createWalletClientSigner(client) — viem WalletClient (Privy, MetaMask, etc.)
291
293
  *
292
- * The agent address is always resolved from the keystore — the single source
294
+ * The agent address is always resolved from the signer — the single source
293
295
  * of truth — so the caller doesn't need to supply (or risk hallucinating) it.
294
- * If `fields.address` is provided it must match the keystore address.
296
+ * If `fields.address` is provided it must match the signer's address.
295
297
  *
296
298
  * @param fields — SIWA message fields (domain, agentId, etc.). `address` is optional.
297
- * @param keystoreConfigOptional keystore configuration override
299
+ * @param signerA Signer implementation (see createKeyringProxySigner, createLocalAccountSigner, createWalletClientSigner)
298
300
  * @returns { message, signature, address } — the plaintext message, EIP-191 signature, and resolved address
301
+ *
302
+ * @example
303
+ * ```typescript
304
+ * import { signSIWAMessage, createLocalAccountSigner } from '@buildersgarden/siwa';
305
+ * import { privateKeyToAccount } from 'viem/accounts';
306
+ *
307
+ * const account = privateKeyToAccount('0x...');
308
+ * const signer = createLocalAccountSigner(account);
309
+ *
310
+ * const { message, signature, address } = await signSIWAMessage({
311
+ * domain: 'example.com',
312
+ * uri: 'https://example.com/login',
313
+ * agentId: 123,
314
+ * agentRegistry: 'eip155:84532:0x...',
315
+ * chainId: 84532,
316
+ * nonce: 'abc123',
317
+ * issuedAt: new Date().toISOString(),
318
+ * }, signer);
319
+ * ```
299
320
  */
300
- export async function signSIWAMessage(fields, keystoreConfig) {
301
- // Import keystore dynamically to avoid circular deps
302
- const { signMessage, getAddress } = await import('./keystore');
303
- // Resolve the address from the keystore — the trusted source of truth
304
- const keystoreAddress = await getAddress(keystoreConfig);
305
- if (!keystoreAddress) {
306
- throw new Error('No wallet found in keystore. Run createWallet() first.');
307
- }
321
+ export async function signSIWAMessage(fields, signer) {
322
+ // Resolve the address from the signer — the trusted source of truth
323
+ const signerAddress = await signer.getAddress();
308
324
  // If the caller supplied an address, verify it matches (defensive check)
309
- if (fields.address && keystoreAddress.toLowerCase() !== fields.address.toLowerCase()) {
310
- throw new Error(`Address mismatch: keystore has ${keystoreAddress}, message claims ${fields.address}`);
325
+ if (fields.address && signerAddress.toLowerCase() !== fields.address.toLowerCase()) {
326
+ throw new Error(`Address mismatch: signer has ${signerAddress}, message claims ${fields.address}`);
311
327
  }
312
328
  const resolvedFields = {
313
329
  ...fields,
314
- address: keystoreAddress,
330
+ address: signerAddress,
315
331
  };
316
332
  const message = buildSIWAMessage(resolvedFields);
317
- // Sign via keystore — private key is loaded, used, and discarded internally
318
- const result = await signMessage(message, keystoreConfig);
319
- return { message, signature: result.signature, address: keystoreAddress };
333
+ // Sign via signer
334
+ const signature = await signer.signMessage(message);
335
+ return { message, signature, address: signerAddress };
320
336
  }
321
337
  /**
322
338
  * Verify a SIWA message + signature.
@@ -342,8 +358,12 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
342
358
  try {
343
359
  // 1. Parse
344
360
  const fields = parseSIWAMessage(message);
345
- // 2. Recover signer
346
- const isValid = await verifyMessage({
361
+ // 2. Verify signature (supports both EOA and ERC-1271 smart wallets)
362
+ // Using client.verifyMessage handles:
363
+ // - EOA signatures (ECDSA recovery)
364
+ // - ERC-1271 smart contract wallets (Safe, Argent, etc.)
365
+ // - ERC-6492 pre-deployed smart wallets
366
+ const isValid = await client.verifyMessage({
347
367
  address: fields.address,
348
368
  message,
349
369
  signature: signature,
@@ -352,6 +372,9 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
352
372
  return fail(fields, SIWAErrorCode.INVALID_SIGNATURE, 'Invalid signature');
353
373
  }
354
374
  const recovered = fields.address;
375
+ // 2b. Detect signer type (EOA vs smart contract account)
376
+ const signerCode = await client.getCode({ address: fields.address });
377
+ const signerType = (signerCode && signerCode !== '0x') ? 'sca' : 'eoa';
355
378
  // 3. Address match is implicit in verifyMessage (it checks against the address)
356
379
  // 4. Domain binding
357
380
  if (fields.domain !== expectedDomain) {
@@ -404,22 +427,7 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
404
427
  return fail(fields, SIWAErrorCode.NOT_REGISTERED, 'Agent is not registered on the ERC-8004 Identity Registry');
405
428
  }
406
429
  if (owner.toLowerCase() !== recovered.toLowerCase()) {
407
- // ERC-1271 fallback for smart contract wallets / EIP-7702 delegated accounts
408
- const messageHash = hashMessage(message);
409
- try {
410
- const magicValue = await client.readContract({
411
- address: owner,
412
- abi: [{ name: 'isValidSignature', type: 'function', stateMutability: 'view', inputs: [{ name: 'hash', type: 'bytes32' }, { name: 'signature', type: 'bytes' }], outputs: [{ name: '', type: 'bytes4' }] }],
413
- functionName: 'isValidSignature',
414
- args: [messageHash, signature],
415
- });
416
- if (magicValue !== '0x1626ba7e') {
417
- return fail(fields, SIWAErrorCode.NOT_OWNER, 'Signer is not the owner of this agent NFT (ERC-1271 check also failed)');
418
- }
419
- }
420
- catch {
421
- return fail(fields, SIWAErrorCode.NOT_OWNER, 'Signer is not the owner of this agent NFT');
422
- }
430
+ return fail(fields, SIWAErrorCode.NOT_OWNER, 'Signer is not the owner of this agent NFT');
423
431
  }
424
432
  // 8. Base result
425
433
  const baseResult = {
@@ -429,9 +437,14 @@ export async function verifySIWA(message, signature, expectedDomain, nonceValid,
429
437
  agentRegistry: fields.agentRegistry,
430
438
  chainId: fields.chainId,
431
439
  verified: 'onchain',
440
+ signerType,
432
441
  };
433
442
  if (!criteria)
434
443
  return baseResult;
444
+ // Signer type policy (checked before fetching metadata for early exit)
445
+ if (criteria.allowedSignerTypes?.length && !criteria.allowedSignerTypes.includes(signerType)) {
446
+ return { ...baseResult, valid: false, code: SIWAErrorCode.CUSTOM_CHECK_FAILED, error: `Signer type '${signerType}' is not in allowed types [${criteria.allowedSignerTypes.join(', ')}]` };
447
+ }
435
448
  const agent = await getAgent(fields.agentId, {
436
449
  registryAddress: registryAddress,
437
450
  client,
package/dist/tba.d.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * tba.ts
3
+ *
4
+ * ERC-6551 Token Bound Account address computation utilities.
5
+ *
6
+ * Pure math — no RPC calls. Useful for platforms that want to verify
7
+ * a signer is specifically a TBA derived from a given agent NFT.
8
+ *
9
+ * The ERC-6551 registry deploys a modified ERC-1167 minimal proxy with
10
+ * immutable data (salt, chainId, tokenContract, tokenId) appended.
11
+ * The CREATE2 address is deterministic from these inputs.
12
+ */
13
+ import { type Address, type Hex } from 'viem';
14
+ /** Canonical ERC-6551 registry address, deployed across all EVM chains. */
15
+ export declare const ERC6551_REGISTRY: "0x000000006551c19487814612e58FE06813775758";
16
+ /**
17
+ * Compute the deterministic ERC-6551 Token Bound Account address for an NFT.
18
+ *
19
+ * Pure math — no RPC call needed. Given the same inputs, the address is
20
+ * the same whether or not the account is deployed.
21
+ *
22
+ * Mirrors the on-chain `ERC6551Registry.account()` function exactly.
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * import { computeTbaAddress } from '@buildersgarden/siwa/tba';
27
+ *
28
+ * const tba = computeTbaAddress({
29
+ * implementation: '0x...TBAImpl',
30
+ * tokenContract: '0x...AgentRegistry',
31
+ * tokenId: 42n,
32
+ * chainId: 84532,
33
+ * });
34
+ * ```
35
+ */
36
+ export declare function computeTbaAddress(params: {
37
+ /** TBA implementation contract address. */
38
+ implementation: Address;
39
+ /** NFT contract address. */
40
+ tokenContract: Address;
41
+ /** NFT token ID. */
42
+ tokenId: bigint;
43
+ /** Chain ID where the NFT lives. */
44
+ chainId: number;
45
+ /** Registry address (defaults to canonical ERC-6551 registry). */
46
+ registry?: Address;
47
+ /** CREATE2 salt (defaults to bytes32(0)). */
48
+ salt?: Hex;
49
+ }): Address;
50
+ /**
51
+ * Check if a signer address matches the expected TBA for an agent NFT.
52
+ *
53
+ * Useful for platforms that want to verify the signer is specifically
54
+ * a TBA derived from the agent's registry, not just any smart contract.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * import { isTbaForAgent } from '@buildersgarden/siwa/tba';
59
+ *
60
+ * const isValid = isTbaForAgent({
61
+ * signerAddress: agent.address as Address,
62
+ * implementation: '0x...TBAImpl',
63
+ * agentRegistry: '0x...AgentRegistry',
64
+ * agentId: 42,
65
+ * chainId: 84532,
66
+ * });
67
+ * ```
68
+ */
69
+ export declare function isTbaForAgent(params: {
70
+ /** The signer address to check. */
71
+ signerAddress: Address;
72
+ /** TBA implementation contract address. */
73
+ implementation: Address;
74
+ /** ERC-8004 agent registry address. */
75
+ agentRegistry: Address;
76
+ /** Agent NFT token ID. */
77
+ agentId: number;
78
+ /** Chain ID. */
79
+ chainId: number;
80
+ /** CREATE2 salt (defaults to bytes32(0)). */
81
+ salt?: Hex;
82
+ }): boolean;
package/dist/tba.js ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * tba.ts
3
+ *
4
+ * ERC-6551 Token Bound Account address computation utilities.
5
+ *
6
+ * Pure math — no RPC calls. Useful for platforms that want to verify
7
+ * a signer is specifically a TBA derived from a given agent NFT.
8
+ *
9
+ * The ERC-6551 registry deploys a modified ERC-1167 minimal proxy with
10
+ * immutable data (salt, chainId, tokenContract, tokenId) appended.
11
+ * The CREATE2 address is deterministic from these inputs.
12
+ */
13
+ import { encodePacked, getContractAddress, } from 'viem';
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+ /** Canonical ERC-6551 registry address, deployed across all EVM chains. */
18
+ export const ERC6551_REGISTRY = '0x000000006551c19487814612e58FE06813775758';
19
+ /**
20
+ * ERC-1167 modified proxy init code prefix + proxy runtime header (20 bytes).
21
+ *
22
+ * Breakdown:
23
+ * 3d60ad80600a3d3981f3 — init code (10 bytes): deploys 0xad bytes of runtime
24
+ * 363d3d373d3d3d363d73 — proxy runtime header (10 bytes): delegatecall setup
25
+ */
26
+ const ERC1167_HEADER = '0x3d60ad80600a3d3981f3363d3d373d3d3d363d73';
27
+ /** ERC-1167 proxy runtime footer (15 bytes): delegatecall + return/revert. */
28
+ const ERC1167_FOOTER = '0x5af43d82803e903d91602b57fd5bf3';
29
+ /** Default salt (bytes32 zero). */
30
+ const DEFAULT_SALT = '0x0000000000000000000000000000000000000000000000000000000000000000';
31
+ // ---------------------------------------------------------------------------
32
+ // Address computation
33
+ // ---------------------------------------------------------------------------
34
+ /**
35
+ * Compute the deterministic ERC-6551 Token Bound Account address for an NFT.
36
+ *
37
+ * Pure math — no RPC call needed. Given the same inputs, the address is
38
+ * the same whether or not the account is deployed.
39
+ *
40
+ * Mirrors the on-chain `ERC6551Registry.account()` function exactly.
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * import { computeTbaAddress } from '@buildersgarden/siwa/tba';
45
+ *
46
+ * const tba = computeTbaAddress({
47
+ * implementation: '0x...TBAImpl',
48
+ * tokenContract: '0x...AgentRegistry',
49
+ * tokenId: 42n,
50
+ * chainId: 84532,
51
+ * });
52
+ * ```
53
+ */
54
+ export function computeTbaAddress(params) {
55
+ const { implementation, tokenContract, tokenId, chainId, registry = ERC6551_REGISTRY, salt = DEFAULT_SALT, } = params;
56
+ // Build the 183-byte creation code (init code) matching ERC6551Registry.sol
57
+ //
58
+ // Layout:
59
+ // [20 bytes] ERC1167_HEADER (init prefix + proxy header)
60
+ // [20 bytes] implementation address
61
+ // [15 bytes] ERC1167_FOOTER (proxy footer)
62
+ // [32 bytes] salt
63
+ // [32 bytes] chainId
64
+ // [32 bytes] tokenContract (address as uint256, left-padded)
65
+ // [32 bytes] tokenId
66
+ const creationCode = encodePacked(['bytes', 'address', 'bytes', 'bytes32', 'uint256', 'uint256', 'uint256'], [
67
+ ERC1167_HEADER,
68
+ implementation,
69
+ ERC1167_FOOTER,
70
+ salt,
71
+ BigInt(chainId),
72
+ BigInt(tokenContract),
73
+ tokenId,
74
+ ]);
75
+ return getContractAddress({
76
+ bytecode: creationCode,
77
+ from: registry,
78
+ opcode: 'CREATE2',
79
+ salt,
80
+ });
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // Agent-specific helpers
84
+ // ---------------------------------------------------------------------------
85
+ /**
86
+ * Check if a signer address matches the expected TBA for an agent NFT.
87
+ *
88
+ * Useful for platforms that want to verify the signer is specifically
89
+ * a TBA derived from the agent's registry, not just any smart contract.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * import { isTbaForAgent } from '@buildersgarden/siwa/tba';
94
+ *
95
+ * const isValid = isTbaForAgent({
96
+ * signerAddress: agent.address as Address,
97
+ * implementation: '0x...TBAImpl',
98
+ * agentRegistry: '0x...AgentRegistry',
99
+ * agentId: 42,
100
+ * chainId: 84532,
101
+ * });
102
+ * ```
103
+ */
104
+ export function isTbaForAgent(params) {
105
+ const expected = computeTbaAddress({
106
+ implementation: params.implementation,
107
+ tokenContract: params.agentRegistry,
108
+ tokenId: BigInt(params.agentId),
109
+ chainId: params.chainId,
110
+ salt: params.salt,
111
+ });
112
+ return expected.toLowerCase() === params.signerAddress.toLowerCase();
113
+ }
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@buildersgarden/siwa",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
8
8
  "default": "./dist/index.js"
9
9
  },
10
+ "./signer": {
11
+ "types": "./dist/signer.d.ts",
12
+ "default": "./dist/signer.js"
13
+ },
10
14
  "./keystore": {
11
15
  "types": "./dist/keystore.d.ts",
12
16
  "default": "./dist/keystore.js"
@@ -46,6 +50,10 @@
46
50
  "./express": {
47
51
  "types": "./dist/express.d.ts",
48
52
  "default": "./dist/express.js"
53
+ },
54
+ "./tba": {
55
+ "types": "./dist/tba.d.ts",
56
+ "default": "./dist/tba.js"
49
57
  }
50
58
  },
51
59
  "main": "./dist/index.js",