Skip to content

Commit 9a676dd

Browse files
committed
fix(cli): update SDK integrations and vault startup
1 parent c761ed5 commit 9a676dd

20 files changed

Lines changed: 336 additions & 136 deletions

‎.changeset/bright-vault-status.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
'@inflowpayai/inflow': patch
33
---
44

5-
Start the local vault daemon before reporting authentication or vault status.
5+
Start the local vault daemon for vault status and interactive authentication status. Report an unauthenticated agent
6+
status without starting a vault that has not been initialized.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@inflowpayai/inflow': patch
3+
---
4+
5+
Update the AEP, ODP, and InFlow SDK dependencies.

‎.changeset/tidy-vault-prompts.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@inflowpayai/inflow': patch
3+
---
4+
5+
Prompt interactive users to unlock the vault before authentication status, combined inspection, and payment cancellation
6+
commands.

‎packages/cli/package.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@
4343
"url": "https://github.com/inflowpayai/inflow-cli/issues"
4444
},
4545
"dependencies": {
46-
"@aep-foundation/agent": "^0.4.0",
46+
"@aep-foundation/agent": "^0.5.0",
4747
"@inflowpayai/mpp": "^0.10.0",
4848
"@inflowpayai/mpp-buyer": "^0.7.2",
49-
"@inflowpayai/x402": "^0.9.0",
50-
"@inflowpayai/x402-buyer": "^0.9.0",
49+
"@inflowpayai/x402": "^0.9.1",
50+
"@inflowpayai/x402-buyer": "^0.9.1",
5151
"@modelcontextprotocol/server": "2.0.0-alpha.4",
5252
"@node-rs/argon2": "^2.0.2",
5353
"@x402/core": "^2.22.0",

‎packages/cli/src/cli.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,13 @@ async function main(): Promise<void> {
176176
const vaultOptions: LocalVaultDaemonClientOptions = { buildId: cliBuildId, cliVersion };
177177
const apiKeyFromEnv = process.env['INFLOW_API_KEY'];
178178
const hasDirectApiKey = (apiKeyFromFlag?.length ?? 0) > 0 || (apiKeyFromEnv?.length ?? 0) > 0;
179+
let hasInitializedVault = true;
179180
if (shouldReconcileVaultDaemon(process.argv, hasDirectApiKey)) {
180181
const status = await readVaultStatusWithoutStarting(vaultOptions);
182+
hasInitializedVault = status.lockState !== 'not_initialized';
181183
if (status.daemonRunning) await ensureLocalVaultDaemon(vaultOptions);
182184
}
183-
if (shouldStartVaultDaemon(process.argv, hasDirectApiKey)) {
185+
if (shouldStartVaultDaemon(process.argv, { hasDirectApiKey, hasInitializedVault, isAgent })) {
184186
await ensureLocalVaultDaemon(vaultOptions);
185187
}
186188
if (shouldUnlockVault(process.argv, { hasDirectApiKey, isAgent })) {

‎packages/cli/src/commands/aep/index.tsx‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,8 +1376,22 @@ async function runRevoke(c: Context, inflow: Inflow, authStorage: AuthStorage):
13761376
? { grantType: options.grantType as string }
13771377
: { credentialId: options.credentialId };
13781378
if ('allGrantTypes' in selector) await revokeService({ ...base, allGrantTypes: true });
1379-
else if ('credentialId' in selector) await revokeService({ ...base, credentialId: selector.credentialId });
1380-
else await revokeService({ ...base, grantType: selector.grantType });
1379+
else if ('credentialId' in selector) {
1380+
const credential = await aepStorage
1381+
.credentials()
1382+
.findCredential(inspect.document.service.did, selector.credentialId);
1383+
if (credential === undefined) {
1384+
throw new CliInputError(
1385+
'AEP_CREDENTIAL_NOT_FOUND',
1386+
`No stored AEP credential exists with identifier ${selector.credentialId}.`,
1387+
);
1388+
}
1389+
await revokeService({
1390+
...base,
1391+
credentialId: selector.credentialId,
1392+
grantType: credential.grantType,
1393+
});
1394+
} else await revokeService({ ...base, grantType: selector.grantType });
13811395
aepStorage.deleteCredentials(inspect.document.service.did, selector);
13821396
const frame = sanitizeDeep({
13831397
revoked: true,

‎packages/cli/src/startup-vault.ts‎

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,33 @@ export function normalizeFormatAssignments(argv: string[]): void {
4545
}
4646
}
4747

48-
export function shouldStartVaultDaemon(argv: readonly string[], hasDirectApiKey = false): boolean {
48+
export function shouldStartVaultDaemon(
49+
argv: readonly string[],
50+
options: { hasDirectApiKey?: boolean; hasInitializedVault?: boolean; isAgent?: boolean } = {},
51+
): boolean {
4952
if (shouldBypassVault(argv)) return false;
5053
const [group, subcommand] = commandPath(argv);
5154
if (group === 'auth') {
52-
return isOneOf(subcommand, 'login', 'logout') || (!hasDirectApiKey && subcommand === 'status');
55+
return (
56+
isOneOf(subcommand, 'login', 'logout') ||
57+
(options.hasDirectApiKey !== true &&
58+
subcommand === 'status' &&
59+
(options.isAgent !== true || options.hasInitializedVault !== false))
60+
);
5361
}
5462
if (group === 'vault') return subcommand === 'status';
5563
if (group === 'aep') return isOneOf(subcommand, 'enroll', 'fetch', 'grant', 'revoke', 'status');
64+
if (group === 'inspect') return shouldConfigureOdpServiceTransport(argv);
5665
if (group === 'odp') return shouldConfigureOdpServiceTransport(argv);
5766
if (group === 'mpp') {
5867
return (
59-
requiresMppLocalState(argv, subcommand) || (!hasDirectApiKey && isOneOf(subcommand, 'pay', 'status', 'supported'))
68+
requiresMppLocalState(argv, subcommand) ||
69+
(options.hasDirectApiKey !== true && isOneOf(subcommand, 'cancel', 'pay', 'status', 'supported'))
6070
);
6171
}
62-
if (group === 'x402') return !hasDirectApiKey && isOneOf(subcommand, 'fetch', 'pay', 'status', 'supported');
72+
if (group === 'x402') {
73+
return options.hasDirectApiKey !== true && isOneOf(subcommand, 'cancel', 'fetch', 'pay', 'status', 'supported');
74+
}
6375
if (group === 'subscriptions') return isOneOf(subcommand, 'cancel', 'fetch');
6476
return false;
6577
}
@@ -71,13 +83,15 @@ export function shouldReconcileVaultDaemon(argv: readonly string[], hasDirectApi
7183
return isOneOf(subcommand, 'login', 'logout') || (!hasDirectApiKey && subcommand === 'status');
7284
}
7385
if (group === 'aep') return isOneOf(subcommand, 'enroll', 'fetch', 'grant', 'revoke', 'status');
86+
if (group === 'inspect') return shouldConfigureOdpServiceTransport(argv);
7487
if (group === 'odp') return shouldConfigureOdpServiceTransport(argv);
7588
if (group === 'mpp') {
7689
return (
77-
requiresMppLocalState(argv, subcommand) || (!hasDirectApiKey && isOneOf(subcommand, 'pay', 'status', 'supported'))
90+
requiresMppLocalState(argv, subcommand) ||
91+
(!hasDirectApiKey && isOneOf(subcommand, 'cancel', 'pay', 'status', 'supported'))
7892
);
7993
}
80-
if (group === 'x402') return !hasDirectApiKey && isOneOf(subcommand, 'fetch', 'pay', 'status', 'supported');
94+
if (group === 'x402') return !hasDirectApiKey && isOneOf(subcommand, 'cancel', 'fetch', 'pay', 'status', 'supported');
8195
if (group === 'subscriptions' && isOneOf(subcommand, 'cancel', 'fetch')) return true;
8296
if (hasDirectApiKey) return false;
8397
if (group === 'balances' || group === 'deposit-addresses') return subcommand === 'list';
@@ -91,17 +105,20 @@ export function shouldUnlockVault(
91105
): boolean {
92106
if (options.isAgent === true || shouldBypassVault(argv)) return false;
93107
const [group, subcommand] = commandPath(argv);
94-
if (group === 'auth') return subcommand === 'login';
108+
if (group === 'auth') {
109+
return subcommand === 'login' || (options.hasDirectApiKey !== true && subcommand === 'status');
110+
}
95111
if (group === 'aep') return isOneOf(subcommand, 'enroll', 'fetch', 'grant', 'revoke', 'status');
112+
if (group === 'inspect') return shouldConfigureOdpServiceTransport(argv);
96113
if (group === 'odp') return shouldConfigureOdpServiceTransport(argv);
97114
if (group === 'mpp') {
98115
return (
99116
requiresMppLocalState(argv, subcommand) ||
100-
(options.hasDirectApiKey !== true && isOneOf(subcommand, 'pay', 'status', 'supported'))
117+
(options.hasDirectApiKey !== true && isOneOf(subcommand, 'cancel', 'pay', 'status', 'supported'))
101118
);
102119
}
103120
if (group === 'x402') {
104-
return options.hasDirectApiKey !== true && isOneOf(subcommand, 'fetch', 'pay', 'status', 'supported');
121+
return options.hasDirectApiKey !== true && isOneOf(subcommand, 'cancel', 'fetch', 'pay', 'status', 'supported');
105122
}
106123
if (group === 'subscriptions' && isOneOf(subcommand, 'cancel', 'fetch')) return true;
107124
if (options.hasDirectApiKey === true) return false;

‎packages/cli/src/utils/api-error.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SecureStorageError } from '@inflowpayai/inflow-core';
12
import { MISSING_SESSION_ERROR } from './assert-session.js';
23

34
interface CliError {
@@ -18,6 +19,20 @@ function apiErrorLike(error: unknown): ApiErrorLike | undefined {
1819
}
1920

2021
export function authenticatedApiError(error: unknown): CliError | undefined {
22+
if (error instanceof SecureStorageError) {
23+
if (error.secureStorageCode === 'vault_locked') {
24+
return {
25+
code: 'VAULT_LOCKED',
26+
message: 'The InFlow vault is locked. A human must run `inflow vault unlock` first.',
27+
};
28+
}
29+
if (error.secureStorageCode === 'vault_not_initialized') {
30+
return {
31+
code: 'VAULT_NOT_INITIALIZED',
32+
message: 'The InFlow vault is not initialized. A human must run `inflow vault unlock` first.',
33+
};
34+
}
35+
}
2136
const apiError = apiErrorLike(error);
2237
if (apiError === undefined) return;
2338
if (apiError.code === 'VERSION_UNSUPPORTED' && typeof apiError.message === 'string') {

‎packages/cli/test/unit/commands/aep/index.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,10 +1205,34 @@ describe('aep commands', () => {
12051205
userId: 'user-1',
12061206
});
12071207
await persisted.identities().saveIdentity(identity);
1208+
if ('credentialId' in options) {
1209+
await persisted.credentials().saveCredential({
1210+
credential: { credential_id: options.credentialId },
1211+
credentialId: options.credentialId,
1212+
expiresAt: '2999-01-01T00:00:00.000Z',
1213+
grantType: 'oauth-bearer',
1214+
issuedAt: '2026-01-01T00:00:00.000Z',
1215+
serviceDid: identity.serviceDid,
1216+
});
1217+
}
12081218

12091219
await expect(__testing.runRevoke(context(options), inflow(), storage)).resolves.toEqual(expected);
12101220
});
12111221

1222+
it('rejects per-credential Revoke when the credential is not stored locally', async () => {
1223+
const storage = new MemoryStorage();
1224+
storage.setApiKey('key');
1225+
const persisted = new AepStorage(storage, {
1226+
platformOrigin: 'https://platform.example',
1227+
userId: 'user-1',
1228+
});
1229+
await persisted.identities().saveIdentity(identity);
1230+
1231+
await expect(
1232+
__testing.runRevoke(context({ credentialId: 'credential-missing' }), inflow(), storage),
1233+
).rejects.toThrow('AEP_CREDENTIAL_NOT_FOUND');
1234+
});
1235+
12121236
it('checks Status and skips approval when enrolling an existing identity', async () => {
12131237
const approvalFetch = vi.fn(() =>
12141238
Promise.resolve(new Response(JSON.stringify({ status: 'APPROVED' }), { status: 200 })),

‎packages/cli/test/unit/commands/mpp/index-runners.test.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { AuthStorage, ICliCapabilitiesResource } from '@inflowpayai/inflow-core';
2-
import { Inflow, InflowApiError, MemoryStorage } from '@inflowpayai/inflow-core';
2+
import { Inflow, InflowApiError, MemoryStorage, SecureStorageError } from '@inflowpayai/inflow-core';
33
import { encode, type MppChallenge, type MppClient, renderChallengeHeader } from '@inflowpayai/mpp';
44
import { afterEach, describe, expect, it, vi } from 'vitest';
55
import { __testing, createMppCli } from '../../../../src/commands/mpp/index.js';
@@ -151,6 +151,16 @@ describe('mpp agent runners', () => {
151151
expect(out).toMatchObject({ code: 'NOT_AUTHENTICATED' });
152152
});
153153

154+
it('runCancelCommand reports a locked vault in agent mode', async () => {
155+
const cancelApproval = vi.fn(() =>
156+
Promise.reject(new SecureStorageError('vault_locked', 'The InFlow vault is locked.')),
157+
);
158+
const { inflow, storage } = authed(makeClient(), cancelApproval);
159+
const ctx = agentCtxReturningError({ approvalId: 'ap-1' }, {});
160+
const out = await runCancelCommand(ctx, inflow, storage);
161+
expect(out).toMatchObject({ code: 'VAULT_LOCKED' });
162+
});
163+
154164
it('runCancelCommand rethrows non-authentication failures', async () => {
155165
const failure = new Error('cancel unavailable');
156166
const cancelApproval = vi.fn(() => Promise.reject(failure));

0 commit comments

Comments
 (0)