Conversation
📝 WalkthroughWalkthroughAdds Taskmarket wallet authentication to Daydreams Router x402 alongside SAW and raw-key options. Updates installer and docs for interactive/non-interactive wallet selection, adds Taskmarket CLI support, implements Taskmarket keystore parsing/decryption and cached account resolution, updates payment backend to resolve Taskmarket on demand, and includes tests and dependency changes. Changes
Sequence DiagramssequenceDiagram
participant User
participant Installer as Installer Script
participant AuthPlugin as x402 Auth Plugin
participant CLI as Taskmarket CLI
participant Keystore as Keystore (disk)
participant DeviceAPI as Taskmarket Device API
participant Wallet as x402-Taskmarket-Wallet
User->>Installer: Run installer (TTY)
Installer->>User: Prompt wallet choice (saw/taskmarket/both)
User->>Installer: Select Taskmarket
Installer->>AuthPlugin: Invoke onboarding with Taskmarket choice
AuthPlugin->>CLI: Check taskmarket availability
alt CLI missing
AuthPlugin->>CLI: Run taskmarket init (provision)
CLI->>Keystore: Create keystore file
end
AuthPlugin->>Keystore: Load keystore (encryptedKey, deviceId, walletAddress)
AuthPlugin->>DeviceAPI: Request device encryption key (deviceId)
DeviceAPI-->>AuthPlugin: Return DEK
AuthPlugin->>Wallet: Decrypt encryptedKey with DEK
Wallet-->>AuthPlugin: Decrypted private key + ownerAddress
AuthPlugin->>AuthPlugin: Generate sentinel credential and patch config
AuthPlugin-->>User: Onboarding complete (sentinel)
sequenceDiagram
participant Client
participant PaymentHandler as x402 Payment Handler
participant Resolver as Backend Resolver
participant TaskWallet as x402-Taskmarket-Wallet
participant DeviceAPI as Taskmarket Device API
participant Signer as Signer/Account
Client->>PaymentHandler: fetch() with payment request
PaymentHandler->>Resolver: resolveBackend()
alt Taskmarket sentinel present
Resolver->>TaskWallet: parse sentinel & createTaskmarketAccount()
TaskWallet->>DeviceAPI: Fetch device encryption key
DeviceAPI-->>TaskWallet: Return DEK
TaskWallet->>TaskWallet: Decrypt private key (AES-256-GCM)
TaskWallet->>Signer: Construct PrivateKeyAccount
Signer-->>Resolver: Account + ownerAddress (cached)
else SAW or local key
Resolver->>Signer: Initialize SAW or local key backend
end
Resolver-->>PaymentHandler: Resolved backend
PaymentHandler->>Signer: Sign request
PaymentHandler-->>Client: Response with payment header
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/agents/x402-payment.ts (1)
805-821: Taskmarket backend resolves account per-request without promise caching.Unlike SAW and private-key paths that cache via
staticBackendPromise, the Taskmarket path callscreateTaskmarketAccount()on every request. WhilecreateTaskmarketAccounthas its own internal TTL-based cache, each call:
- Reads the keystore file from disk
- Checks the account cache
- Creates a new
WalletClientinstanceThe account cache prevents redundant DEK fetches, but keystore reads and wallet client creations still occur per-request. If high throughput is expected, consider caching the resolved backend similar to the SAW path.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/agents/x402-payment.ts` around lines 805 - 821, The Taskmarket branch in resolveBackend calls createTaskmarketAccount on every request causing filesystem reads and new WalletClient creation; change it to cache the resolved SigningBackend promise similarly to the SAW/private-key path (use the existing staticBackendPromise pattern) so subsequent calls reuse the same promise/result until it expires or owner changes; implement storing the promise when taskmarketConfig is first used, reuse it on later resolveBackend calls, and when the returned ownerAddress differs from taskmarketBackendAddress update taskmarketBackendAddress, log the change, and invalidate/replace the cached promise so a fresh backend is created.docs/providers/x402.md (1)
25-29: Consider varying sentence structure for readability.Three consecutive items start with "Phase". A minor style improvement would be to vary the phrasing:
📝 Suggested rewrite
-1. **Wallet choice**: if a TTY is available, the script asks whether to install SAW, Taskmarket, or both. In non-interactive shells it defaults to installing both. -2. **Phase 1 — SAW daemon**: when selected, downloads SAW binaries, generates an EVM wallet key on-device, writes a conservative spending policy, and starts the daemon (systemd on Linux, LaunchAgent on macOS). -3. **Phase 2 — OpenClaw gateway**: installs the CLI globally from a prebuilt release tarball (fast, no build tools needed). -4. **Phase 2.25 — Taskmarket CLI**: when selected, installs the `taskmarket` binary globally so Taskmarket wallet onboarding can rely on it being present. -5. **Phase 3 — Onboarding**: runs `openclaw onboard --auth-choice x402` if a TTY is available; otherwise prints the manual command. +1. **Wallet choice**: if a TTY is available, the script asks whether to install SAW, Taskmarket, or both. In non-interactive shells it defaults to installing both. +2. **SAW daemon** (Phase 1): when selected, downloads SAW binaries, generates an EVM wallet key on-device, writes a conservative spending policy, and starts the daemon (systemd on Linux, LaunchAgent on macOS). +3. **OpenClaw gateway** (Phase 2): installs the CLI globally from a prebuilt release tarball (fast, no build tools needed). +4. **Taskmarket CLI** (Phase 2.25): when selected, installs the `taskmarket` binary globally so Taskmarket wallet onboarding can rely on it being present. +5. **Onboarding** (Phase 3): runs `openclaw onboard --auth-choice x402` if a TTY is available; otherwise prints the manual command.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/providers/x402.md` around lines 25 - 29, The three consecutive list items starting with "Phase" reduce readability; update the bullet text in docs/providers/x402.md so phrasing varies while preserving meaning and referenced symbols: keep "Phase 1 — SAW daemon" as-is, reword "Phase 2 — OpenClaw gateway" to something like "Install OpenClaw gateway (Phase 2)" referencing the CLI install from the prebuilt release tarball, change "Phase 2.25 — Taskmarket CLI" to "Install Taskmarket CLI (Phase 2.25)" and keep its note about installing the taskmarket binary globally, and rephrase "Phase 3 — Onboarding" to "Onboarding (Phase 3)" while retaining the TTY behavior and the exact command `openclaw onboard --auth-choice x402` for interactive shells; ensure the sequence and technical details remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@extensions/daydreams-x402-auth/index.ts`:
- Around line 304-525: The listed utilities (resolveHomePath,
normalizeTaskmarketApiUrl, getErrorCode, decodeTaskmarketEncryptedKey,
decryptTaskmarketPrivateKey) are duplicated; extract them into a shared module
and have both this extension and src/agents/x402-taskmarket-wallet import that
module (or import from the core agents package), updating package dependency and
cross-package import paths to use .js if required; ensure the shared
decryptTaskmarketPrivateKey keeps the common logic and surface (throwing generic
Errors) and update callers like verifyTaskmarketWalletIntegrity and
verifyTaskmarketDeviceKeyAccess to use the centralized functions (preserve
names: resolveHomePath, normalizeTaskmarketApiUrl, getErrorCode,
decodeTaskmarketEncryptedKey, decryptTaskmarketPrivateKey).
---
Nitpick comments:
In `@docs/providers/x402.md`:
- Around line 25-29: The three consecutive list items starting with "Phase"
reduce readability; update the bullet text in docs/providers/x402.md so phrasing
varies while preserving meaning and referenced symbols: keep "Phase 1 — SAW
daemon" as-is, reword "Phase 2 — OpenClaw gateway" to something like "Install
OpenClaw gateway (Phase 2)" referencing the CLI install from the prebuilt
release tarball, change "Phase 2.25 — Taskmarket CLI" to "Install Taskmarket CLI
(Phase 2.25)" and keep its note about installing the taskmarket binary globally,
and rephrase "Phase 3 — Onboarding" to "Onboarding (Phase 3)" while retaining
the TTY behavior and the exact command `openclaw onboard --auth-choice x402` for
interactive shells; ensure the sequence and technical details remain unchanged.
In `@src/agents/x402-payment.ts`:
- Around line 805-821: The Taskmarket branch in resolveBackend calls
createTaskmarketAccount on every request causing filesystem reads and new
WalletClient creation; change it to cache the resolved SigningBackend promise
similarly to the SAW/private-key path (use the existing staticBackendPromise
pattern) so subsequent calls reuse the same promise/result until it expires or
owner changes; implement storing the promise when taskmarketConfig is first
used, reuse it on later resolveBackend calls, and when the returned ownerAddress
differs from taskmarketBackendAddress update taskmarketBackendAddress, log the
change, and invalidate/replace the cached promise so a fresh backend is created.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 63de425e-9f5e-4a20-8f37-c13cbb0361fe
📒 Files selected for processing (9)
docs/install/index.mddocs/providers/x402.mdextensions/daydreams-x402-auth/index.tsextensions/daydreams-x402-auth/package.jsonscripts/install-openclaw-fork.shsrc/agents/x402-payment.test.tssrc/agents/x402-payment.tssrc/agents/x402-taskmarket-wallet.test.tssrc/agents/x402-taskmarket-wallet.ts
| function normalizeTaskmarketApiUrl(value: string): string { | ||
| const raw = value.trim() || DEFAULT_TASKMARKET_API_URL; | ||
| const withProtocol = raw.startsWith("http") ? raw : `https://${raw}`; | ||
| return withProtocol.replace(/\/+$/, ""); | ||
| } | ||
|
|
||
| function resolveHomePath(value: string): string { | ||
| const trimmed = value.trim(); | ||
| if (!trimmed) { | ||
| return trimmed; | ||
| } | ||
| if (trimmed === "~") { | ||
| return os.homedir(); | ||
| } | ||
| if (trimmed.startsWith("~/")) { | ||
| return path.join(os.homedir(), trimmed.slice(2)); | ||
| } | ||
| return path.resolve(trimmed); | ||
| } | ||
|
|
||
| function toBase64Url(value: string): string { | ||
| return Buffer.from(value, "utf8") | ||
| .toString("base64") | ||
| .replace(/\+/g, "-") | ||
| .replace(/\//g, "_") | ||
| .replace(/=+$/g, ""); | ||
| } | ||
|
|
||
| type TaskmarketKeystore = { | ||
| encryptedKey: string; | ||
| walletAddress: string; | ||
| deviceId: string; | ||
| apiToken: string; | ||
| }; | ||
|
|
||
| function getErrorCode(error: unknown): string | undefined { | ||
| return error && typeof error === "object" && "code" in error && typeof error.code === "string" | ||
| ? error.code | ||
| : undefined; | ||
| } | ||
|
|
||
| function buildTaskmarketSentinel(payload: { keystorePath: string; apiUrl?: string }): string { | ||
| const record: Record<string, unknown> = { | ||
| v: TASKMARKET_SENTINEL_VERSION, | ||
| keystorePath: payload.keystorePath, | ||
| }; | ||
| const apiUrl = payload.apiUrl ? normalizeTaskmarketApiUrl(payload.apiUrl) : ""; | ||
| if (apiUrl && apiUrl !== FALLBACK_TASKMARKET_API_URL) { | ||
| record.apiUrl = apiUrl; | ||
| } | ||
| return `${TASKMARKET_SENTINEL_PREFIX}${toBase64Url(JSON.stringify(record))}`; | ||
| } | ||
|
|
||
| function buildSawSentinel(walletName: string, socketPath: string): string { | ||
| return `saw:${walletName}@${socketPath}`; | ||
| } | ||
|
|
||
| async function loadTaskmarketKeystore(keystorePath: string): Promise<TaskmarketKeystore> { | ||
| const resolvedPath = resolveHomePath(keystorePath); | ||
| if (!resolvedPath) { | ||
| throw new Error("Taskmarket keystore path is required"); | ||
| } | ||
|
|
||
| let raw: string; | ||
| try { | ||
| raw = await fs.readFile(resolvedPath, "utf8"); | ||
| } catch (error) { | ||
| if (getErrorCode(error) === "ENOENT") { | ||
| throw new Error( | ||
| `Taskmarket keystore not found at ${resolvedPath}. Run taskmarket init first.`, | ||
| ); | ||
| } | ||
| throw new Error( | ||
| `Taskmarket keystore at ${resolvedPath} could not be read: ${error instanceof Error ? error.message : String(error)}`, | ||
| ); | ||
| } | ||
|
|
||
| let parsed: unknown; | ||
| try { | ||
| parsed = JSON.parse(raw); | ||
| } catch { | ||
| throw new Error(`Taskmarket keystore at ${resolvedPath} is not valid JSON.`); | ||
| } | ||
|
|
||
| const record = parsed as Record<string, unknown>; | ||
| const encryptedKey = | ||
| typeof record.encryptedKey === "string" ? record.encryptedKey.trim() : undefined; | ||
| const walletAddress = | ||
| typeof record.walletAddress === "string" ? record.walletAddress.trim() : undefined; | ||
| const deviceId = typeof record.deviceId === "string" ? record.deviceId.trim() : undefined; | ||
| const apiToken = typeof record.apiToken === "string" ? record.apiToken.trim() : undefined; | ||
| const normalizedWalletAddress = normalizeAddress(walletAddress); | ||
|
|
||
| if (!encryptedKey || !deviceId || !apiToken || !normalizedWalletAddress) { | ||
| throw new Error( | ||
| `Taskmarket keystore at ${resolvedPath} is missing required fields (encryptedKey, walletAddress, deviceId, apiToken).`, | ||
| ); | ||
| } | ||
|
|
||
| return { | ||
| encryptedKey, | ||
| walletAddress: normalizedWalletAddress, | ||
| deviceId, | ||
| apiToken, | ||
| }; | ||
| } | ||
|
|
||
| async function verifyTaskmarketDeviceKeyAccess( | ||
| apiUrl: string, | ||
| keystore: TaskmarketKeystore, | ||
| ): Promise<string> { | ||
| const normalizedUrl = normalizeTaskmarketApiUrl(apiUrl); | ||
| const endpoint = `${normalizedUrl}/api/devices/${encodeURIComponent(keystore.deviceId)}/key`; | ||
| let response: Response; | ||
| try { | ||
| response = await fetch(endpoint, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json" }, | ||
| body: JSON.stringify({ deviceId: keystore.deviceId, apiToken: keystore.apiToken }), | ||
| }); | ||
| } catch (error) { | ||
| throw new Error( | ||
| `Could not contact Taskmarket API at ${normalizedUrl}: ${error instanceof Error ? error.message : String(error)}`, | ||
| ); | ||
| } | ||
| if (!response.ok) { | ||
| const detail = await response.text().catch(() => ""); | ||
| if (response.status === 401 || response.status === 403) { | ||
| throw new Error( | ||
| "Taskmarket device token was rejected. Reprovision wallet via taskmarket init and rerun onboarding.", | ||
| ); | ||
| } | ||
| if (response.status === 404) { | ||
| throw new Error( | ||
| `Taskmarket device ${keystore.deviceId} was not found. Reprovision wallet via taskmarket init and rerun onboarding.`, | ||
| ); | ||
| } | ||
| throw new Error( | ||
| `Taskmarket device key probe failed (${response.status}). ${detail.slice(0, 180)} Reprovision your wallet with taskmarket init if needed.`, | ||
| ); | ||
| } | ||
|
|
||
| const parsed = (await response.json().catch(() => null)) as { | ||
| deviceEncryptionKey?: unknown; | ||
| } | null; | ||
| const dek = | ||
| parsed && typeof parsed.deviceEncryptionKey === "string" | ||
| ? parsed.deviceEncryptionKey.trim() | ||
| : ""; | ||
| if (!/^[0-9a-fA-F]{64}$/.test(dek)) { | ||
| throw new Error( | ||
| "Taskmarket device key probe returned an invalid key payload. Reprovision wallet via taskmarket init.", | ||
| ); | ||
| } | ||
| return dek; | ||
| } | ||
|
|
||
| function decodeTaskmarketEncryptedKey(encryptedHex: string): { | ||
| iv: Buffer; | ||
| tag: Buffer; | ||
| ciphertext: Buffer; | ||
| } { | ||
| const data = Buffer.from(encryptedHex, "hex"); | ||
| if (data.length <= 28) { | ||
| throw new Error("Taskmarket encrypted keystore payload is too short."); | ||
| } | ||
| const iv = data.subarray(0, 12); | ||
| const tag = data.subarray(12, 28); | ||
| const ciphertext = data.subarray(28); | ||
| if (ciphertext.length === 0) { | ||
| throw new Error("Taskmarket encrypted keystore payload is empty."); | ||
| } | ||
| return { iv, tag, ciphertext }; | ||
| } | ||
|
|
||
| function decryptTaskmarketPrivateKey(deviceEncryptionKeyHex: string, encryptedHex: string): string { | ||
| const key = Buffer.from(deviceEncryptionKeyHex, "hex"); | ||
| if (key.length !== 32) { | ||
| throw new Error( | ||
| "Taskmarket device key payload is invalid. Reprovision wallet via taskmarket init.", | ||
| ); | ||
| } | ||
|
|
||
| const { iv, tag, ciphertext } = decodeTaskmarketEncryptedKey(encryptedHex); | ||
| try { | ||
| const decipher = createDecipheriv("aes-256-gcm", key, iv); | ||
| decipher.setAuthTag(tag); | ||
| const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString( | ||
| "utf8", | ||
| ); | ||
| const normalized = decrypted.trim().startsWith("0X") | ||
| ? `0x${decrypted.trim().slice(2)}` | ||
| : decrypted.trim(); | ||
| if (!PRIVATE_KEY_REGEX.test(normalized)) { | ||
| throw new Error("Decrypted Taskmarket key is not a valid private key."); | ||
| } | ||
| return normalized; | ||
| } catch (error) { | ||
| if ( | ||
| error instanceof Error && | ||
| error.message === "Decrypted Taskmarket key is not a valid private key." | ||
| ) { | ||
| throw error; | ||
| } | ||
| throw new Error( | ||
| "Taskmarket keystore could not be decrypted with the fetched device key. Reprovision wallet via taskmarket init.", | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| function verifyTaskmarketWalletIntegrity( | ||
| keystore: TaskmarketKeystore, | ||
| deviceEncryptionKey: string, | ||
| ): void { | ||
| const privateKey = decryptTaskmarketPrivateKey(deviceEncryptionKey, keystore.encryptedKey); | ||
| const account = privateKeyToAccount(privateKey as `0x${string}`); | ||
| if (account.address.toLowerCase() !== keystore.walletAddress.toLowerCase()) { | ||
| throw new Error( | ||
| "Taskmarket keystore address mismatch after decryption. Reprovision wallet via taskmarket init and rerun onboarding.", | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if both files exist
echo "=== Checking file existence ==="
ls -lh extensions/daydreams-x402-auth/index.ts src/agents/x402-taskmarket-wallet.ts 2>&1
# Extract function names from both files
echo -e "\n=== Functions in x402-taskmarket-wallet.ts ==="
rg -n "^(export\s+)?(async\s+)?function\s+\w+" src/agents/x402-taskmarket-wallet.ts | head -30
echo -e "\n=== Functions in daydreams-x402-auth/index.ts ==="
rg -n "^(export\s+)?(async\s+)?function\s+\w+" extensions/daydreams-x402-auth/index.ts | head -30Repository: daydreamsai/dreaming-claw
Length of output: 2918
🏁 Script executed:
#!/bin/bash
# Get specific function implementations to compare
echo "=== resolveHomePath in x402-taskmarket-wallet.ts ==="
sed -n '/^function resolveHomePath/,/^}/p' src/agents/x402-taskmarket-wallet.ts
echo -e "\n=== resolveHomePath in daydreams-x402-auth/index.ts ==="
sed -n '/^function resolveHomePath/,/^}/p' extensions/daydreams-x402-auth/index.ts
echo -e "\n=== normalizeTaskmarketApiUrl in x402-taskmarket-wallet.ts ==="
sed -n '/^function normalizeTaskmarketApiUrl/,/^}/p' src/agents/x402-taskmarket-wallet.ts
echo -e "\n=== normalizeTaskmarketApiUrl in daydreams-x402-auth/index.ts ==="
sed -n '/^function normalizeTaskmarketApiUrl/,/^}/p' extensions/daydreams-x402-auth/index.tsRepository: daydreamsai/dreaming-claw
Length of output: 1152
🏁 Script executed:
#!/bin/bash
# Check imports at the top of both files to understand dependencies
echo "=== Imports in x402-taskmarket-wallet.ts (first 30 lines) ==="
head -30 src/agents/x402-taskmarket-wallet.ts
echo -e "\n=== Imports in daydreams-x402-auth/index.ts (first 40 lines) ==="
head -40 extensions/daydreams-x402-auth/index.tsRepository: daydreamsai/dreaming-claw
Length of output: 2574
🏁 Script executed:
#!/bin/bash
# Compare the normalizeApiUrl vs normalizeTaskmarketApiUrl
echo "=== normalizeApiUrl in x402-taskmarket-wallet.ts ==="
sed -n '/^function normalizeApiUrl/,/^}/p' src/agents/x402-taskmarket-wallet.ts
echo -e "\n=== decodeTaskmarketEncryptedKey in x402-taskmarket-wallet.ts ==="
sed -n '/^function decodeTaskmarketEncryptedKey/,/^}/p' src/agents/x402-taskmarket-wallet.ts | head -30
echo -e "\n=== decodeTaskmarketEncryptedKey in daydreams-x402-auth/index.ts ==="
sed -n '/^function decodeTaskmarketEncryptedKey/,/^}/p' extensions/daydreams-x402-auth/index.ts | head -30
echo -e "\n=== decryptTaskmarketPrivateKey in x402-taskmarket-wallet.ts ==="
sed -n '/^function decryptTaskmarketPrivateKey/,/^}/p' src/agents/x402-taskmarket-wallet.ts | head -30
echo -e "\n=== decryptTaskmarketPrivateKey in daydreams-x402-auth/index.ts ==="
sed -n '/^function decryptTaskmarketPrivateKey/,/^}/p' extensions/daydreams-x402-auth/index.ts | head -30Repository: daydreamsai/dreaming-claw
Length of output: 3023
🏁 Script executed:
#!/bin/bash
# Check getErrorCode and loadTaskmarketKeystore
echo "=== getErrorCode in x402-taskmarket-wallet.ts ==="
sed -n '/^function getErrorCode/,/^}/p' src/agents/x402-taskmarket-wallet.ts
echo -e "\n=== getErrorCode in daydreams-x402-auth/index.ts ==="
sed -n '/^function getErrorCode/,/^}/p' extensions/daydreams-x402-auth/index.ts
echo -e "\n=== loadTaskmarketKeystore signatures ==="
echo "x402-taskmarket-wallet.ts:"
grep -A 2 "^async function loadTaskmarketKeystore" src/agents/x402-taskmarket-wallet.ts
echo -e "\ndaydreams-x402-auth/index.ts:"
grep -A 2 "^async function loadTaskmarketKeystore" extensions/daydreams-x402-auth/index.tsRepository: daydreamsai/dreaming-claw
Length of output: 959
🏁 Script executed:
#!/bin/bash
# Check directory structure to understand cross-package import feasibility
echo "=== Directory structure ==="
ls -la extensions/daydreams-x402-auth/ | head -15
echo -e "\n=== Package boundaries ==="
cat extensions/daydreams-x402-auth/package.json 2>/dev/null | head -20
cat src/agents/package.json 2>/dev/null | head -20
</parameter>Repository: daydreamsai/dreaming-claw
Length of output: 870
Extract shared Taskmarket utilities to avoid code duplication.
The following functions are duplicated between this file and src/agents/x402-taskmarket-wallet.ts:
resolveHomePath(identical)normalizeApiUrl/normalizeTaskmarketApiUrl(identical logic)getErrorCode(identical)decodeTaskmarketEncryptedKey(identical)decryptTaskmarketPrivateKey(identical core logic, differs only in error type)
This violates the DRY principle. Consider either importing these utilities from the core module or extracting them to a shared module that both can depend on. Note that the extension package currently doesn't depend on the core agents module, so this would require adding that dependency and using .js extensions for cross-package imports.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@extensions/daydreams-x402-auth/index.ts` around lines 304 - 525, The listed
utilities (resolveHomePath, normalizeTaskmarketApiUrl, getErrorCode,
decodeTaskmarketEncryptedKey, decryptTaskmarketPrivateKey) are duplicated;
extract them into a shared module and have both this extension and
src/agents/x402-taskmarket-wallet import that module (or import from the core
agents package), updating package dependency and cross-package import paths to
use .js if required; ensure the shared decryptTaskmarketPrivateKey keeps the
common logic and surface (throwing generic Errors) and update callers like
verifyTaskmarketWalletIntegrity and verifyTaskmarketDeviceKeyAccess to use the
centralized functions (preserve names: resolveHomePath,
normalizeTaskmarketApiUrl, getErrorCode, decodeTaskmarketEncryptedKey,
decryptTaskmarketPrivateKey).
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@extensions/daydreams-x402-auth/index.ts`:
- Around line 388-401: Validate encryptedKey more strictly immediately after
parsing: when constructing encryptedKey from parsed/record, ensure it matches
the same hex format and minimum length checks used in
src/agents/x402-taskmarket-wallet.ts (e.g., only [0-9a-fA-F] and required
byte/char length) before calling normalizeAddress or performing any
network/device operations; if the check fails, throw a clear Error mentioning
the keystore and that encryptedKey is malformed so the failure is local and not
treated as an onboarding/device error. Reference variables: parsed/record,
encryptedKey, normalizeAddress.
- Around line 833-835: The code currently calls ensureTaskmarketCliAvailable()
unconditionally before checking for an existing keystore, which incorrectly
forces the CLI even when the JSON keystore is present; remove that top-level
call and instead call ensureTaskmarketCliAvailable() only inside the branch in
ensureTaskmarketWalletProvisioned() that handles creating/provisioning the
keystore (i.e., the "missing-file" path), so
loadTaskmarketKeystore(keystorePath) can succeed without the CLI when the
keystore already exists.
- Around line 304-307: normalizeTaskmarketApiUrl currently accepts and upgrades
bare hostnames but does not reject explicit http:// endpoints; update this
function to parse the final URL (use the URL constructor) after normalizing
(default to DEFAULT_TASKMARKET_API_URL and prepend https:// when missing), and
if url.protocol === "http:" then throw a clear error unless url.hostname is an
allowed localhost host (e.g. "localhost", "127.0.0.1", "::1") so local dev still
works; reference normalizeTaskmarketApiUrl and DEFAULT_TASKMARKET_API_URL when
making this change.
In `@src/agents/x402-taskmarket-wallet.ts`:
- Around line 80-83: The normalizeApiUrl function currently accepts plain http
and can leak credentials; update normalizeApiUrl to reject non-HTTPS URLs by
parsing the input (function normalizeApiUrl) and throwing an error if the scheme
is "http" or missing and would result in "http://" except allow explicit
localhost/127.0.0.1 (and [::1]) addresses as an explicit dev exception;
normalize to https:// for other hosts and still strip trailing slashes. Apply
the same strict HTTPS guard to the auth extension’s copy of this normalizer so
both places behave identically.
- Around line 246-260: The POST in fetchTaskmarketDeviceEncryptionKey can hang;
add a request deadline by using an AbortController and passing its signal to
params.fetchFn, start a timeout (e.g., 5–10s) that calls controller.abort(), and
clear the timeout once the fetch resolves; catch the abort/timeout case
(AbortError) and throw a descriptive error so createTaskmarketAccount() can
retry. Ensure the AbortController is created inside
fetchTaskmarketDeviceEncryptionKey, the timeout is cleared after response or
error, and the abort signal is included in the fetch options with existing
headers/body logic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2c7c3629-6134-4f74-9b13-0a8f8a4dddcd
📒 Files selected for processing (3)
extensions/daydreams-x402-auth/index.tssrc/agents/x402-taskmarket-wallet.test.tssrc/agents/x402-taskmarket-wallet.ts
| function normalizeTaskmarketApiUrl(value: string): string { | ||
| const raw = value.trim() || DEFAULT_TASKMARKET_API_URL; | ||
| const withProtocol = raw.startsWith("http") ? raw : `https://${raw}`; | ||
| return withProtocol.replace(/\/+$/, ""); |
There was a problem hiding this comment.
Enforce HTTPS for Taskmarket API URLs during onboarding.
If TASKMARKET_API_URL is set to http://..., this flow will post the device token and receive the device key over cleartext. Reject non-HTTPS endpoints here, with an explicit localhost exception if you need local development.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@extensions/daydreams-x402-auth/index.ts` around lines 304 - 307,
normalizeTaskmarketApiUrl currently accepts and upgrades bare hostnames but does
not reject explicit http:// endpoints; update this function to parse the final
URL (use the URL constructor) after normalizing (default to
DEFAULT_TASKMARKET_API_URL and prepend https:// when missing), and if
url.protocol === "http:" then throw a clear error unless url.hostname is an
allowed localhost host (e.g. "localhost", "127.0.0.1", "::1") so local dev still
works; reference normalizeTaskmarketApiUrl and DEFAULT_TASKMARKET_API_URL when
making this change.
| const record = parsed as Record<string, unknown>; | ||
| const encryptedKey = | ||
| typeof record.encryptedKey === "string" ? record.encryptedKey.trim() : undefined; | ||
| const walletAddress = | ||
| typeof record.walletAddress === "string" ? record.walletAddress.trim() : undefined; | ||
| const deviceId = typeof record.deviceId === "string" ? record.deviceId.trim() : undefined; | ||
| const apiToken = typeof record.apiToken === "string" ? record.apiToken.trim() : undefined; | ||
| const normalizedWalletAddress = normalizeAddress(walletAddress); | ||
|
|
||
| if (!encryptedKey || !deviceId || !apiToken || !normalizedWalletAddress) { | ||
| throw new Error( | ||
| `Taskmarket keystore at ${resolvedPath} is missing required fields (encryptedKey, walletAddress, deviceId, apiToken).`, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Tighten encryptedKey validation before the network probe.
This copy only checks that encryptedKey is present. Non-hex or too-short payloads will fall through to a later decrypt/device-key failure, which turns a local keystore problem into a misleading onboarding error. The stricter hex/length checks in src/agents/x402-taskmarket-wallet.ts should be mirrored here.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@extensions/daydreams-x402-auth/index.ts` around lines 388 - 401, Validate
encryptedKey more strictly immediately after parsing: when constructing
encryptedKey from parsed/record, ensure it matches the same hex format and
minimum length checks used in src/agents/x402-taskmarket-wallet.ts (e.g., only
[0-9a-fA-F] and required byte/char length) before calling normalizeAddress or
performing any network/device operations; if the check fails, throw a clear
Error mentioning the keystore and that encryptedKey is malformed so the failure
is local and not treated as an onboarding/device error. Reference variables:
parsed/record, encryptedKey, normalizeAddress.
| ensureTaskmarketCliAvailable(); | ||
| await ensureTaskmarketWalletProvisioned(ctx, keystorePath); | ||
| const keystore = await loadTaskmarketKeystore(keystorePath); |
There was a problem hiding this comment.
Don’t require the Taskmarket CLI when the keystore already exists.
taskmarket init is the only CLI-dependent step, but this check runs before you even look for the keystore. That blocks otherwise valid setups where the JSON file is already present and the runtime path works without the CLI. Move the availability check into the missing-file branch inside ensureTaskmarketWalletProvisioned().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@extensions/daydreams-x402-auth/index.ts` around lines 833 - 835, The code
currently calls ensureTaskmarketCliAvailable() unconditionally before checking
for an existing keystore, which incorrectly forces the CLI even when the JSON
keystore is present; remove that top-level call and instead call
ensureTaskmarketCliAvailable() only inside the branch in
ensureTaskmarketWalletProvisioned() that handles creating/provisioning the
keystore (i.e., the "missing-file" path), so
loadTaskmarketKeystore(keystorePath) can succeed without the CLI when the
keystore already exists.
| function normalizeApiUrl(value: string): string { | ||
| const raw = value.trim() || DEFAULT_TASKMARKET_API_URL; | ||
| const withProtocol = raw.startsWith("http") ? raw : `https://${raw}`; | ||
| return withProtocol.replace(/\/+$/, ""); |
There was a problem hiding this comment.
Reject plaintext Taskmarket API URLs.
http://... is accepted unchanged here, so a sentinel or env override can send the device token and returned device key over cleartext. Please fail closed for non-HTTPS endpoints, with an explicit localhost exception if local dev needs it. The auth extension’s copy of this normalizer needs the same guard.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/agents/x402-taskmarket-wallet.ts` around lines 80 - 83, The
normalizeApiUrl function currently accepts plain http and can leak credentials;
update normalizeApiUrl to reject non-HTTPS URLs by parsing the input (function
normalizeApiUrl) and throwing an error if the scheme is "http" or missing and
would result in "http://" except allow explicit localhost/127.0.0.1 (and [::1])
addresses as an explicit dev exception; normalize to https:// for other hosts
and still strip trailing slashes. Apply the same strict HTTPS guard to the auth
extension’s copy of this normalizer so both places behave identically.
| async function fetchTaskmarketDeviceEncryptionKey(params: { | ||
| apiUrl: string; | ||
| deviceId: string; | ||
| apiToken: string; | ||
| fetchFn: typeof fetch; | ||
| }): Promise<string> { | ||
| const apiUrl = normalizeApiUrl(params.apiUrl); | ||
| const url = `${apiUrl}/api/devices/${encodeURIComponent(params.deviceId)}/key`; | ||
| let response: Response; | ||
| try { | ||
| response = await params.fetchFn(url, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json" }, | ||
| body: JSON.stringify({ deviceId: params.deviceId, apiToken: params.apiToken }), | ||
| }); |
There was a problem hiding this comment.
Add a deadline to the device-key request.
If this POST stalls, createTaskmarketAccount() can hang indefinitely on the request path and the retry logic never runs. Please wrap the request in a timeout/cancellation path so network wedges fail fast.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/agents/x402-taskmarket-wallet.ts` around lines 246 - 260, The POST in
fetchTaskmarketDeviceEncryptionKey can hang; add a request deadline by using an
AbortController and passing its signal to params.fetchFn, start a timeout (e.g.,
5–10s) that calls controller.abort(), and clear the timeout once the fetch
resolves; catch the abort/timeout case (AbortError) and throw a descriptive
error so createTaskmarketAccount() can retry. Ensure the AbortController is
created inside fetchTaskmarketDeviceEncryptionKey, the timeout is cleared after
response or error, and the abort signal is included in the fetch options with
existing headers/body logic.
Summary by CodeRabbit
New Features
Documentation
Tests