run402 2.39.4 → 2.41.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.
Files changed (40) hide show
  1. package/cli.mjs +1 -1
  2. package/core-dist/control-plane-session.js +114 -0
  3. package/lib/operator.mjs +203 -12
  4. package/lib/org.mjs +205 -103
  5. package/lib/transfer.mjs +94 -27
  6. package/package.json +1 -1
  7. package/sdk/core-dist/control-plane-session.js +114 -0
  8. package/sdk/dist/control-plane-credentials.d.ts +45 -0
  9. package/sdk/dist/control-plane-credentials.d.ts.map +1 -0
  10. package/sdk/dist/control-plane-credentials.js +57 -0
  11. package/sdk/dist/control-plane-credentials.js.map +1 -0
  12. package/sdk/dist/errors.d.ts +31 -1
  13. package/sdk/dist/errors.d.ts.map +1 -1
  14. package/sdk/dist/errors.js +59 -0
  15. package/sdk/dist/errors.js.map +1 -1
  16. package/sdk/dist/index.d.ts +6 -2
  17. package/sdk/dist/index.d.ts.map +1 -1
  18. package/sdk/dist/index.js +4 -2
  19. package/sdk/dist/index.js.map +1 -1
  20. package/sdk/dist/kernel.d.ts.map +1 -1
  21. package/sdk/dist/kernel.js +4 -1
  22. package/sdk/dist/kernel.js.map +1 -1
  23. package/sdk/dist/namespaces/operator-session.d.ts +223 -0
  24. package/sdk/dist/namespaces/operator-session.d.ts.map +1 -0
  25. package/sdk/dist/namespaces/operator-session.js +230 -0
  26. package/sdk/dist/namespaces/operator-session.js.map +1 -0
  27. package/sdk/dist/namespaces/operator.d.ts +63 -0
  28. package/sdk/dist/namespaces/operator.d.ts.map +1 -1
  29. package/sdk/dist/namespaces/operator.js +51 -0
  30. package/sdk/dist/namespaces/operator.js.map +1 -1
  31. package/sdk/dist/namespaces/org.d.ts +55 -23
  32. package/sdk/dist/namespaces/org.d.ts.map +1 -1
  33. package/sdk/dist/namespaces/org.js +117 -52
  34. package/sdk/dist/namespaces/org.js.map +1 -1
  35. package/sdk/dist/namespaces/org.types.d.ts +37 -1
  36. package/sdk/dist/namespaces/org.types.d.ts.map +1 -1
  37. package/sdk/dist/namespaces/transfers.d.ts +58 -0
  38. package/sdk/dist/namespaces/transfers.d.ts.map +1 -1
  39. package/sdk/dist/namespaces/transfers.js +40 -0
  40. package/sdk/dist/namespaces/transfers.js.map +1 -1
package/lib/org.mjs CHANGED
@@ -1,42 +1,49 @@
1
1
  import { getSdk } from "./sdk.mjs";
2
- import { reportSdkError } from "./sdk-errors.mjs";
2
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
3
3
  import {
4
4
  normalizeArgv,
5
5
  assertKnownFlags,
6
6
  flagValue,
7
+ parseIntegerFlag,
7
8
  requirePositionalCount,
8
9
  } from "./argparse.mjs";
9
10
 
10
- const HELP = `run402 org org-owned control plane: identity, membership, roles
11
+ const ROLE_LIST = "owner | admin | developer | billing | viewer";
12
+
13
+ const HELP = `run402 org — org-owned control plane: identity, membership, invites
11
14
 
12
15
  Usage:
13
- run402 org <subcommand> [args...]
16
+ run402 org whoami
17
+ run402 org list
18
+ run402 org audit <billing_account> [--limit N] [--before <cursor>]
19
+ run402 org member list <billing_account>
20
+ run402 org member add <billing_account> <wallet> [--role <role>]
21
+ run402 org member role <billing_account> <principal_id> <role>
22
+ run402 org member rm <billing_account> <principal_id>
23
+ run402 org invite list <billing_account>
24
+ run402 org invite create <billing_account> <email> [--role <role>] [--ttl-hours N]
25
+ run402 org invite rm <billing_account> <principal_id>
14
26
 
15
27
  Subcommands:
16
- whoami Resolved principal + org memberships (GET /agent/v1/whoami)
17
- list Orgs you are a member of
18
- members <billing_account> Members + roles of an org
19
- add-member <billing_account> <wallet> [--role <role>]
20
- Add a member by wallet (owner-gated; role defaults to developer)
21
- set-role <billing_account> <principal_id> <role>
22
- Change a member's role (owner-gated)
23
- remove-member <billing_account> <principal_id>
24
- Remove a member (owner-gated)
28
+ whoami Resolved principal + org memberships (GET /agent/v1/whoami)
29
+ list Orgs you are a member of
30
+ member Manage members (list, add, role, rm) mutations require owner
31
+ invite Manage email invites (list, create, rm) — mutations require owner
32
+ audit Control-plane audit trail for an org (admin+)
25
33
 
26
34
  Notes:
27
35
  - A wallet AUTHENTICATES; the org (billing account) owns projects. Membership/role authorizes.
28
- - Roles: owner > admin > developer > billing > viewer. Member changes need an active owner.
29
- - "add-member" is by WALLET (email-first invite is a separate, not-yet-shipped flow).
36
+ - Roles: ${ROLE_LIST}. Member/invite changes need an active owner.
30
37
  - Removing/demoting the org's only active owner fails with 409 LAST_OWNER.
31
38
  - JSON in, JSON out.
32
39
 
33
40
  Examples:
34
41
  run402 org whoami
35
- run402 org list
36
- run402 org members ba_abc
37
- run402 org add-member ba_abc 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --role admin
38
- run402 org set-role ba_abc prn_xyz owner
39
- run402 org remove-member ba_abc prn_xyz
42
+ run402 org member list ba_abc
43
+ run402 org member add ba_abc 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --role admin
44
+ run402 org member role ba_abc prn_xyz owner
45
+ run402 org invite create ba_abc dev@example.com --role developer
46
+ run402 org audit ba_abc --limit 50
40
47
  `;
41
48
 
42
49
  const SUB_HELP = {
@@ -46,57 +53,46 @@ Usage:
46
53
  run402 org whoami
47
54
 
48
55
  Calls GET /agent/v1/whoami. Returns the control-plane principal (id/type/displayName/createdAt),
49
- authenticator_id, and every org membership with role + status. This is the REMOTE
50
- identity; for local wallet/profile state use \`run402 status\`.
56
+ authenticator_id, and every org membership with role + status. REMOTE identity; for local
57
+ wallet/profile state use 'run402 status'.
51
58
  `,
52
59
  list: `run402 org list — orgs you are a member of
53
60
 
54
61
  Usage:
55
62
  run402 org list
56
-
57
- Returns each org (billing account) you belong to with your role and membership status.
58
- `,
59
- members: `run402 org members — members + roles of an org
60
-
61
- Usage:
62
- run402 org members <billing_account>
63
-
64
- Arguments:
65
- <billing_account> Org (billing account) id, e.g. ba_...
66
63
  `,
67
- "add-member": `run402 org add-member — add a member by wallet (owner-gated)
64
+ member: `run402 org member — manage org members
68
65
 
69
66
  Usage:
70
- run402 org add-member <billing_account> <wallet> [--role <role>]
67
+ run402 org member list <billing_account>
68
+ run402 org member add <billing_account> <wallet> [--role <role>]
69
+ run402 org member role <billing_account> <principal_id> <role>
70
+ run402 org member rm <billing_account> <principal_id>
71
71
 
72
- Arguments:
73
- <billing_account> Org (billing account) id
74
- <wallet> EVM address or named wallet (a new wallet is provisioned as a human principal)
75
-
76
- Options:
77
- --role <role> owner | admin | developer | billing | viewer (default: developer)
72
+ Roles: ${ROLE_LIST} (add defaults to developer). Mutations require an active owner.
73
+ Demoting/removing the org's only active owner fails with 409 LAST_OWNER.
78
74
  `,
79
- "set-role": `run402 org set-rolechange a member's role (owner-gated)
75
+ invite: `run402 org invitemanage email invites
80
76
 
81
77
  Usage:
82
- run402 org set-role <billing_account> <principal_id> <role>
83
-
84
- Arguments:
85
- <billing_account> Org (billing account) id
86
- <principal_id> Member principal id (prn_..., from \`run402 org members\`)
87
- <role> owner | admin | developer | billing | viewer
78
+ run402 org invite list <billing_account>
79
+ run402 org invite create <billing_account> <email> [--role <role>] [--ttl-hours N]
80
+ run402 org invite rm <billing_account> <principal_id>
88
81
 
89
- Demoting the org's only active owner fails with 409 LAST_OWNER.
82
+ An invite is claimed at the recipient's first login. Mutations require an active owner
83
+ (plus step-up when driven by a control-plane session).
90
84
  `,
91
- "remove-member": `run402 org remove-memberremove a member (owner-gated)
85
+ audit: `run402 org auditcontrol-plane audit trail
92
86
 
93
87
  Usage:
94
- run402 org remove-member <billing_account> <principal_id>
88
+ run402 org audit <billing_account> [--limit N] [--before <cursor>]
95
89
 
96
- Removing the org's only active owner fails with 409 LAST_OWNER.
90
+ Requires an admin+ membership on the org. Newest-first; page with --before.
97
91
  `,
98
92
  };
99
93
 
94
+ // ── Top-level: whoami / list / audit ───────────────────────────────────────────
95
+
100
96
  async function whoami(args) {
101
97
  const a = normalizeArgv(args);
102
98
  assertKnownFlags(a, ["--help", "-h"]);
@@ -119,70 +115,169 @@ async function list(args) {
119
115
  }
120
116
  }
121
117
 
122
- async function members(args) {
118
+ async function audit(args) {
123
119
  const a = normalizeArgv(args);
124
- assertKnownFlags(a, ["--help", "-h"]);
125
- const [ba] = requirePositionalCount(a, [], {
120
+ const valueFlags = ["--limit", "--before"];
121
+ assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
122
+ const [ba] = requirePositionalCount(a, valueFlags, {
126
123
  min: 1,
127
124
  max: 1,
128
- command: "run402 org members <billing_account>",
125
+ command: "run402 org audit <billing_account>",
129
126
  missing: "Missing <billing_account>.",
130
127
  });
128
+ const limitFlag = flagValue(a, "--limit");
129
+ const before = flagValue(a, "--before");
130
+ const limit = limitFlag === null ? undefined : parseIntegerFlag("--limit", limitFlag, { min: 1, max: 1000 });
131
131
  try {
132
- console.log(JSON.stringify({ members: await getSdk().org.members(ba) }, null, 2));
132
+ const events = await getSdk().org.audit(ba, { limit, before: before ?? undefined });
133
+ console.log(JSON.stringify({ events }, null, 2));
133
134
  } catch (err) {
134
135
  reportSdkError(err);
135
136
  }
136
137
  }
137
138
 
138
- async function addMember(args) {
139
- const a = normalizeArgv(args);
140
- assertKnownFlags(a, ["--role", "--help", "-h"], ["--role"]);
141
- const role = flagValue(a, "--role");
142
- const [ba, wallet] = requirePositionalCount(a, ["--role"], {
143
- min: 2,
144
- max: 2,
145
- command: "run402 org add-member <billing_account> <wallet> [--role <role>]",
146
- missing: "Missing <billing_account> and/or <wallet>.",
147
- });
148
- try {
149
- const res = await getSdk().org.addMember(ba, { wallet, role: role || undefined });
150
- console.log(JSON.stringify(res, null, 2));
151
- } catch (err) {
152
- reportSdkError(err);
139
+ // ── Member group ───────────────────────────────────────────────────────────────
140
+
141
+ async function runMember(args) {
142
+ const memberAction = args[0];
143
+ const rest = args.slice(1);
144
+ if (!memberAction || memberAction === "--help" || memberAction === "-h") {
145
+ console.log(SUB_HELP.member);
146
+ process.exit(memberAction ? 0 : 1);
147
+ }
148
+ if (rest.includes("--help") || rest.includes("-h")) {
149
+ console.log(SUB_HELP.member);
150
+ process.exit(0);
153
151
  }
154
- }
155
152
 
156
- async function setRole(args) {
157
- const a = normalizeArgv(args);
158
- assertKnownFlags(a, ["--help", "-h"]);
159
- const [ba, principalId, role] = requirePositionalCount(a, [], {
160
- min: 3,
161
- max: 3,
162
- command: "run402 org set-role <billing_account> <principal_id> <role>",
163
- missing: "Missing <billing_account>, <principal_id>, and/or <role>.",
164
- });
165
- try {
166
- console.log(JSON.stringify(await getSdk().org.setRole(ba, principalId, role), null, 2));
167
- } catch (err) {
168
- reportSdkError(err);
153
+ if (memberAction === "list") {
154
+ const a = normalizeArgv(rest);
155
+ assertKnownFlags(a, ["--help", "-h"]);
156
+ const [ba] = requirePositionalCount(a, [], {
157
+ min: 1, max: 1, command: "run402 org member list <billing_account>", missing: "Missing <billing_account>.",
158
+ });
159
+ try {
160
+ console.log(JSON.stringify({ members: await getSdk().org.members.list(ba) }, null, 2));
161
+ } catch (err) {
162
+ reportSdkError(err);
163
+ }
164
+ return;
165
+ }
166
+
167
+ if (memberAction === "add") {
168
+ const a = normalizeArgv(rest);
169
+ assertKnownFlags(a, ["--role", "--help", "-h"], ["--role"]);
170
+ const role = flagValue(a, "--role");
171
+ const [ba, wallet] = requirePositionalCount(a, ["--role"], {
172
+ min: 2, max: 2, command: "run402 org member add <billing_account> <wallet> [--role <role>]",
173
+ missing: "Missing <billing_account> and/or <wallet>.",
174
+ });
175
+ try {
176
+ const res = await getSdk().org.members.add(ba, { wallet, role: role || undefined });
177
+ console.log(JSON.stringify(res, null, 2));
178
+ } catch (err) {
179
+ reportSdkError(err);
180
+ }
181
+ return;
182
+ }
183
+
184
+ if (memberAction === "role") {
185
+ const a = normalizeArgv(rest);
186
+ assertKnownFlags(a, ["--help", "-h"]);
187
+ const [ba, principalId, role] = requirePositionalCount(a, [], {
188
+ min: 3, max: 3, command: "run402 org member role <billing_account> <principal_id> <role>",
189
+ missing: "Missing <billing_account>, <principal_id>, and/or <role>.",
190
+ });
191
+ try {
192
+ console.log(JSON.stringify(await getSdk().org.members.setRole(ba, principalId, role), null, 2));
193
+ } catch (err) {
194
+ reportSdkError(err);
195
+ }
196
+ return;
197
+ }
198
+
199
+ if (memberAction === "rm") {
200
+ const a = normalizeArgv(rest);
201
+ assertKnownFlags(a, ["--help", "-h"]);
202
+ const [ba, principalId] = requirePositionalCount(a, [], {
203
+ min: 2, max: 2, command: "run402 org member rm <billing_account> <principal_id>",
204
+ missing: "Missing <billing_account> and/or <principal_id>.",
205
+ });
206
+ try {
207
+ console.log(JSON.stringify(await getSdk().org.members.revoke(ba, principalId), null, 2));
208
+ } catch (err) {
209
+ reportSdkError(err);
210
+ }
211
+ return;
169
212
  }
213
+
214
+ fail({ code: "BAD_USAGE", message: `Unknown 'org member' action: ${memberAction}. Try list | add | role | rm.` });
170
215
  }
171
216
 
172
- async function removeMember(args) {
173
- const a = normalizeArgv(args);
174
- assertKnownFlags(a, ["--help", "-h"]);
175
- const [ba, principalId] = requirePositionalCount(a, [], {
176
- min: 2,
177
- max: 2,
178
- command: "run402 org remove-member <billing_account> <principal_id>",
179
- missing: "Missing <billing_account> and/or <principal_id>.",
180
- });
181
- try {
182
- console.log(JSON.stringify(await getSdk().org.removeMember(ba, principalId), null, 2));
183
- } catch (err) {
184
- reportSdkError(err);
217
+ // ── Invite group ─────────────────────────────────────────────────────────────────
218
+
219
+ async function runInvite(args) {
220
+ const inviteAction = args[0];
221
+ const rest = args.slice(1);
222
+ if (!inviteAction || inviteAction === "--help" || inviteAction === "-h") {
223
+ console.log(SUB_HELP.invite);
224
+ process.exit(inviteAction ? 0 : 1);
225
+ }
226
+ if (rest.includes("--help") || rest.includes("-h")) {
227
+ console.log(SUB_HELP.invite);
228
+ process.exit(0);
185
229
  }
230
+
231
+ if (inviteAction === "list") {
232
+ const a = normalizeArgv(rest);
233
+ assertKnownFlags(a, ["--help", "-h"]);
234
+ const [ba] = requirePositionalCount(a, [], {
235
+ min: 1, max: 1, command: "run402 org invite list <billing_account>", missing: "Missing <billing_account>.",
236
+ });
237
+ try {
238
+ console.log(JSON.stringify({ invites: await getSdk().org.invites.list(ba) }, null, 2));
239
+ } catch (err) {
240
+ reportSdkError(err);
241
+ }
242
+ return;
243
+ }
244
+
245
+ if (inviteAction === "create") {
246
+ const a = normalizeArgv(rest);
247
+ const valueFlags = ["--role", "--ttl-hours"];
248
+ assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
249
+ const role = flagValue(a, "--role");
250
+ const ttlFlag = flagValue(a, "--ttl-hours");
251
+ const [ba, email] = requirePositionalCount(a, valueFlags, {
252
+ min: 2, max: 2, command: "run402 org invite create <billing_account> <email> [--role <role>]",
253
+ missing: "Missing <billing_account> and/or <email>.",
254
+ });
255
+ const inviteTtlHours = ttlFlag === null ? undefined : parseIntegerFlag("--ttl-hours", ttlFlag, { min: 1, max: 8760 });
256
+ try {
257
+ const res = await getSdk().org.invites.create(ba, { email, role: role || "developer", inviteTtlHours });
258
+ console.log(JSON.stringify(res, null, 2));
259
+ } catch (err) {
260
+ reportSdkError(err);
261
+ }
262
+ return;
263
+ }
264
+
265
+ if (inviteAction === "rm") {
266
+ const a = normalizeArgv(rest);
267
+ assertKnownFlags(a, ["--help", "-h"]);
268
+ const [ba, principalId] = requirePositionalCount(a, [], {
269
+ min: 2, max: 2, command: "run402 org invite rm <billing_account> <principal_id>",
270
+ missing: "Missing <billing_account> and/or <principal_id>.",
271
+ });
272
+ try {
273
+ console.log(JSON.stringify(await getSdk().org.invites.revoke(ba, principalId), null, 2));
274
+ } catch (err) {
275
+ reportSdkError(err);
276
+ }
277
+ return;
278
+ }
279
+
280
+ fail({ code: "BAD_USAGE", message: `Unknown 'org invite' action: ${inviteAction}. Try list | create | rm.` });
186
281
  }
187
282
 
188
283
  export async function run(sub, args) {
@@ -190,17 +285,24 @@ export async function run(sub, args) {
190
285
  console.log(HELP);
191
286
  process.exit(0);
192
287
  }
193
- if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
194
- console.log(SUB_HELP[sub] || HELP);
288
+ // Nested groups use `if (sub === ...)` (not `case`) so the sync test extracts
289
+ // their leaf actions via the dedicated memberAction/inviteAction parsers.
290
+ if (sub === "member" || sub === "members") {
291
+ await runMember(args ?? []);
292
+ return;
293
+ }
294
+ if (sub === "invite" || sub === "invites") {
295
+ await runInvite(args ?? []);
296
+ return;
297
+ }
298
+ if (Array.isArray(args) && (args.includes("--help") || args.includes("-h")) && SUB_HELP[sub]) {
299
+ console.log(SUB_HELP[sub]);
195
300
  process.exit(0);
196
301
  }
197
302
  switch (sub) {
198
303
  case "whoami": await whoami(args); break;
199
304
  case "list": await list(args); break;
200
- case "members": await members(args); break;
201
- case "add-member": await addMember(args); break;
202
- case "set-role": await setRole(args); break;
203
- case "remove-member": await removeMember(args); break;
305
+ case "audit": await audit(args); break;
204
306
  default:
205
307
  console.error(`Unknown subcommand: ${sub}\n`);
206
308
  console.log(HELP);
package/lib/transfer.mjs CHANGED
@@ -13,18 +13,21 @@ import {
13
13
  const HELP = `run402 transfer — Two-party project transfer (v1.59)
14
14
 
15
15
  Usage:
16
- run402 transfer init --to <wallet> [--project <id>] [--billing-policy migrate] [--message <text>] [--kysigned <record_id>]
16
+ run402 transfer init --to <wallet|email> [--project <id>] [--billing-policy migrate] [--message <text>] [--kysigned <record_id>]
17
17
  run402 transfer preview <transfer_id>
18
18
  run402 transfer list [--incoming | --outgoing] [--limit N] [--offset N]
19
19
  run402 transfer accept <transfer_id>
20
- run402 transfer cancel <transfer_id> [--reason <text>]
20
+ run402 transfer claim <transfer_id> [--into <billing_account_id>]
21
+ run402 transfer cancel <transfer_id> [--reason <text>] [--handoff]
21
22
 
22
23
  Subcommands:
23
- init Initiate a transfer to a new owner (you must currently own the project)
24
- preview Fetch the safe review document (either party may view)
25
- list List pending transfers (incoming by default, or --outgoing)
26
- accept Accept an incoming transfer (your wallet must be the to_wallet)
27
- cancel Cancel a pending transfer (either party may cancel)
24
+ init Initiate ownership change. --to <wallet> = two-party wallet transfer;
25
+ --to <email> = email->org handoff (the recipient claims it).
26
+ preview Fetch the safe review document (add --handoff for an email handoff)
27
+ list List pending transfers (incoming default, --outgoing, or --handoffs)
28
+ accept Accept an incoming wallet transfer (your wallet must be the to_wallet)
29
+ claim Claim an incoming email handoff into an org (--into <id>; omit = new org)
30
+ cancel Cancel a pending transfer/handoff (--handoff routes to a handoff)
28
31
 
29
32
  Notes:
30
33
  - Owner-side mutations on a project with a pending transfer return 409
@@ -38,7 +41,7 @@ const SUB_HELP = {
38
41
  init: `run402 transfer init — Initiate a project transfer
39
42
 
40
43
  Usage:
41
- run402 transfer init --to <wallet> [--project <id>] [--billing-policy migrate] [--message <text>] [--kysigned <record_id>]
44
+ run402 transfer init --to <wallet|email> [--project <id>] [--billing-policy migrate] [--message <text>] [--kysigned <record_id>]
42
45
 
43
46
  Options:
44
47
  --project <id> Project id (defaults to the active project)
@@ -85,10 +88,19 @@ project, enqueues notifications to both parties, and stamps a
85
88
  cancel: `run402 transfer cancel — Cancel a pending transfer
86
89
 
87
90
  Usage:
88
- run402 transfer cancel <transfer_id> [--reason <text>]
91
+ run402 transfer cancel <transfer_id> [--reason <text>] [--handoff]
89
92
 
90
93
  Either the from_wallet or the to_wallet may cancel. Already-processed
91
- transfers return 409 TRANSFER_ALREADY_PROCESSED.
94
+ transfers return 409 TRANSFER_ALREADY_PROCESSED. Pass --handoff to cancel an
95
+ email->org handoff instead of a wallet transfer.
96
+ `,
97
+ claim: `run402 transfer claim — Claim an incoming email handoff
98
+
99
+ Usage:
100
+ run402 transfer claim <transfer_id> [--into <billing_account_id>]
101
+
102
+ Claims a handoff addressed to your email into an org you own. Omit --into to
103
+ claim into a brand-new org. This is the email-handoff analog of 'accept'.
92
104
  `,
93
105
  };
94
106
 
@@ -122,16 +134,27 @@ async function init(args) {
122
134
  const message = flagValue(parsedArgs, "--message");
123
135
  const kysigned = flagValue(parsedArgs, "--kysigned");
124
136
 
125
- allowanceAuthHeaders(`/projects/v1/${projectId}/transfers`);
137
+ // One noun, two rails: an email recipient routes to the email->org handoff;
138
+ // a wallet recipient routes to the two-party wallet transfer.
139
+ const isEmail = toWallet.includes("@");
140
+ allowanceAuthHeaders(
141
+ isEmail ? `/projects/v1/${projectId}/handoffs` : `/projects/v1/${projectId}/transfers`,
142
+ );
126
143
 
127
144
  try {
128
- const res = await getSdk().admin.transfers.initiate({
129
- projectId,
130
- toWallet,
131
- billingPolicy: billingPolicy ?? undefined,
132
- message: message ?? undefined,
133
- kysignedRecordId: kysigned ?? undefined,
134
- });
145
+ const res = isEmail
146
+ ? await getSdk().admin.transfers.initiateHandoff({
147
+ projectId,
148
+ toEmail: toWallet,
149
+ message: message ?? undefined,
150
+ })
151
+ : await getSdk().admin.transfers.initiate({
152
+ projectId,
153
+ toWallet,
154
+ billingPolicy: billingPolicy ?? undefined,
155
+ message: message ?? undefined,
156
+ kysignedRecordId: kysigned ?? undefined,
157
+ });
135
158
  console.log(JSON.stringify(res, null, 2));
136
159
  } catch (err) {
137
160
  reportSdkError(err);
@@ -140,16 +163,19 @@ async function init(args) {
140
163
 
141
164
  async function preview(args) {
142
165
  const parsedArgs = normalizeArgv(args);
143
- assertKnownFlags(parsedArgs, ["--help", "-h"]);
166
+ assertKnownFlags(parsedArgs, ["--handoff", "--help", "-h"]);
144
167
  const positionals = positionalArgs(parsedArgs);
145
168
  if (positionals.length !== 1) {
146
- fail({ code: "BAD_USAGE", message: "Usage: run402 transfer preview <transfer_id>" });
169
+ fail({ code: "BAD_USAGE", message: "Usage: run402 transfer preview <transfer_id> [--handoff]" });
147
170
  }
148
171
  const transferId = positionals[0];
149
- allowanceAuthHeaders(`/agent/v1/transfers/${transferId}`);
172
+ const handoff = parsedArgs.includes("--handoff");
173
+ allowanceAuthHeaders(`/agent/v1/${handoff ? "handoffs" : "transfers"}/${transferId}`);
150
174
 
151
175
  try {
152
- const data = await getSdk().admin.transfers.preview(transferId);
176
+ const data = handoff
177
+ ? await getSdk().admin.transfers.previewHandoff(transferId)
178
+ : await getSdk().admin.transfers.preview(transferId);
153
179
  console.log(JSON.stringify(data, null, 2));
154
180
  } catch (err) {
155
181
  reportSdkError(err);
@@ -159,11 +185,24 @@ async function preview(args) {
159
185
  async function list(args) {
160
186
  const parsedArgs = normalizeArgv(args);
161
187
  const valueFlags = ["--limit", "--offset"];
162
- assertKnownFlags(parsedArgs, [...valueFlags, "--incoming", "--outgoing", "--help", "-h"], valueFlags);
188
+ assertKnownFlags(parsedArgs, [...valueFlags, "--incoming", "--outgoing", "--handoffs", "--help", "-h"], valueFlags);
163
189
  const extra = positionalArgs(parsedArgs, valueFlags);
164
190
  if (extra.length > 0) {
165
191
  fail({ code: "BAD_USAGE", message: `Unexpected argument for transfer list: ${extra[0]}` });
166
192
  }
193
+
194
+ // Email->org handoffs ride the same rail but have their own incoming inbox.
195
+ if (parsedArgs.includes("--handoffs")) {
196
+ allowanceAuthHeaders("/agent/v1/handoffs/incoming");
197
+ try {
198
+ const handoffs = await getSdk().admin.transfers.listIncomingHandoffs();
199
+ console.log(JSON.stringify({ kind: "handoffs", handoffs }, null, 2));
200
+ } catch (err) {
201
+ reportSdkError(err);
202
+ }
203
+ return;
204
+ }
205
+
167
206
  const incoming = parsedArgs.includes("--incoming");
168
207
  const outgoing = parsedArgs.includes("--outgoing");
169
208
  if (incoming && outgoing) {
@@ -216,17 +255,42 @@ async function accept(args) {
216
255
  async function cancel(args) {
217
256
  const parsedArgs = normalizeArgv(args);
218
257
  const valueFlags = ["--reason"];
219
- assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
258
+ assertKnownFlags(parsedArgs, [...valueFlags, "--handoff", "--help", "-h"], valueFlags);
220
259
  const positionals = positionalArgs(parsedArgs, valueFlags);
221
260
  if (positionals.length !== 1) {
222
- fail({ code: "BAD_USAGE", message: "Usage: run402 transfer cancel <transfer_id> [--reason <text>]" });
261
+ fail({ code: "BAD_USAGE", message: "Usage: run402 transfer cancel <transfer_id> [--reason <text>] [--handoff]" });
223
262
  }
224
263
  const transferId = positionals[0];
225
264
  const reason = flagValue(parsedArgs, "--reason");
226
- allowanceAuthHeaders(`/agent/v1/transfers/${transferId}/cancel`);
265
+ const handoff = parsedArgs.includes("--handoff");
266
+ allowanceAuthHeaders(`/agent/v1/${handoff ? "handoffs" : "transfers"}/${transferId}/cancel`);
267
+
268
+ try {
269
+ const data = handoff
270
+ ? await getSdk().admin.transfers.cancelHandoff(transferId)
271
+ : await getSdk().admin.transfers.cancel(transferId, reason ?? undefined);
272
+ console.log(JSON.stringify(data, null, 2));
273
+ } catch (err) {
274
+ reportSdkError(err);
275
+ }
276
+ }
277
+
278
+ async function claim(args) {
279
+ const parsedArgs = normalizeArgv(args);
280
+ const valueFlags = ["--into"];
281
+ assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
282
+ const positionals = positionalArgs(parsedArgs, valueFlags);
283
+ if (positionals.length !== 1) {
284
+ fail({ code: "BAD_USAGE", message: "Usage: run402 transfer claim <transfer_id> [--into <billing_account_id>]" });
285
+ }
286
+ const transferId = positionals[0];
287
+ const into = flagValue(parsedArgs, "--into");
288
+ allowanceAuthHeaders(`/agent/v1/handoffs/${transferId}/claim`);
227
289
 
228
290
  try {
229
- const data = await getSdk().admin.transfers.cancel(transferId, reason ?? undefined);
291
+ const data = await getSdk().admin.transfers.claimHandoff(transferId, {
292
+ billingAccountId: into ?? undefined,
293
+ });
230
294
  console.log(JSON.stringify(data, null, 2));
231
295
  } catch (err) {
232
296
  reportSdkError(err);
@@ -255,6 +319,9 @@ export async function run(sub, args) {
255
319
  case "accept":
256
320
  await accept(args);
257
321
  return;
322
+ case "claim":
323
+ await claim(args);
324
+ return;
258
325
  case "cancel":
259
326
  await cancel(args);
260
327
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "2.39.4",
3
+ "version": "2.41.0",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {