getcofund 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -23,6 +23,9 @@ With npx, put `--` first when passing flags, or npm takes flags such as `--help`
23
23
  cofund keys init [--force]
24
24
  cofund me
25
25
  cofund invite accept [--invite <id>] [--company <name>]
26
+ cofund invite create --email <email> --first-name <name> --last-name <name> --position <title> [--admin] [--chains A,B] [--yes]
27
+ cofund policies create --vault <id> --name <name> --signers <id|email,...> --threshold <n>
28
+ (--token <address|BTC> --max-amount <base units> | --wrapper <address>) [--type <PolicyType>] [--yes]
26
29
  cofund requests [list]
27
30
  cofund requests show <id>
28
31
  cofund requests sign <id> [--yes]
@@ -61,6 +64,11 @@ It rebuilds the EIP-712 message itself under the pinned domain, signs locally, a
61
64
 
62
65
  What it takes from the server: which on-chain user id (EVM) or account xpub (BTC) belongs to which named policy signer, and an invite's `callerId` and `authId` (a wrong one only makes the chain refuse it). `approve` receives each signer with its id or xpub, so a script holding its own roster can check the pairing.
63
66
 
67
+ An admin creates policies and invites with keys of their own:
68
+ - `policies create` builds the activation from the flags: the signers by member id or email (an EVM activation names their `onchainId`, a BTC one their account xpubs), the threshold, and the token and cap per transfer in base units or the wrapper (function `entry`). A transfer policy is `Operational_Expense` and a call policy `Treasury_Management` unless `--type` says otherwise. It signs under the pinned domain and company, with the BTC key's Cofund leaf on a Bitcoin vault. The policy activates at once when one admin may activate it; otherwise it awaits the other admins as a `policy` request.
69
+ - `invite create` asks which user id the email's seat registers (an existing account keeps its own), signs `InviteUser` on each of the cofund's EVM vault chains (or `--chains`), 29 days out, and sends the invite. A member seat is claimable at once; an admin seat awaits the other admins' co-signatures where the chain asks for them, one `invite` request per chain.
70
+ - Both show what they will sign and ask, or take `--yes`.
71
+
64
72
  `stocks buy` quotes the pool, sets the minimum output 1% under the quote, signs the swap and proposes it on the cofund's Stocks policy. The other signers approve it in the app or with `requests sign`.
65
73
 
66
74
  ## SDK
@@ -97,6 +105,55 @@ for (const request of await api.requests(cofundId)) {
97
105
 
98
106
  `proposeTransfer` and `proposeCall` take an id the caller picks (`id`, `authId`); a retry with the same id returns the same request, so the client retries them on a lost response. `signTransferProposal` and `signCallProposal` sign the proposer's own intent.
99
107
 
108
+ An admin's writes work the same way: `signPolicyCreation` and `signInviteCreation` build the activation or each chain's `InviteUser` from your inputs and sign it locally, and `createPolicy` and `createInvite` send it under the id you picked.
109
+
110
+ ```js
111
+ import { randomUUID } from "node:crypto";
112
+ import { signInviteCreation, signPolicyCreation } from "getcofund";
113
+
114
+ const company = keystore.cofunds[cofundId].company;
115
+ const signers = { evm: evmWallet(keystore), btc: btcMessageSigner(keystore) };
116
+ const [vault] = await api.vaults(cofundId);
117
+ const roster = await api.members(cofundId);
118
+
119
+ const policy = await signPolicyCreation(
120
+ {
121
+ id: randomUUID(),
122
+ vault,
123
+ name: "Payouts",
124
+ type: "Operational_Expense",
125
+ signers: roster.filter((m) => m.isAdmin),
126
+ threshold: 2,
127
+ token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
128
+ maxAmount: "1000000000", // 1,000 USDC per transfer
129
+ },
130
+ company,
131
+ pins,
132
+ signers,
133
+ );
134
+ await api.createPolicy(cofundId, policy); // AWAITING_SIGNATURES, or active at once
135
+
136
+ const id = randomUUID();
137
+ const { userId } = await api.inviteUserId(cofundId, "nia@example.com");
138
+ const invite = await signInviteCreation(
139
+ {
140
+ id,
141
+ callerId: me.id,
142
+ userId: userId ?? id,
143
+ email: "nia@example.com",
144
+ firstName: "Nia",
145
+ lastName: "Okafor",
146
+ position: "Treasurer",
147
+ isAdmin: false,
148
+ chains: ["ETHEREUM", "ROBINHOOD"],
149
+ },
150
+ company,
151
+ pins,
152
+ signers.evm,
153
+ );
154
+ await api.createInvite(cofundId, invite);
155
+ ```
156
+
100
157
  ## Webhooks
101
158
 
102
159
  `cofund webhooks add https://bot.example.com/cofund` subscribes an endpoint for your key's agent, to every event or those named with `--events`, and prints its signing secret once. The URL must be https, on a host that resolves only to public addresses.
@@ -73,6 +73,13 @@ export declare const PolicyTypeSchema: z.ZodEnum<{
73
73
  Treasury_Management: "Treasury_Management";
74
74
  }>;
75
75
  export type PolicyType = z.infer<typeof PolicyTypeSchema>;
76
+ /** The types the API lists and creates: not the app's own deposit and invoice policies. */
77
+ export declare const ApiPolicyTypeSchema: z.ZodEnum<{
78
+ Contractor_Stipend: "Contractor_Stipend";
79
+ Operational_Expense: "Operational_Expense";
80
+ Treasury_Management: "Treasury_Management";
81
+ }>;
82
+ export type ApiPolicyType = z.infer<typeof ApiPolicyTypeSchema>;
76
83
  export declare const PolicyStatusSchema: z.ZodEnum<{
77
84
  PENDING: "PENDING";
78
85
  ACTIVE: "ACTIVE";
@@ -431,6 +438,7 @@ export declare const ApiPolicySchema: z.ZodObject<{
431
438
  export type ApiPolicy = z.infer<typeof ApiPolicySchema>;
432
439
  export declare const ApiMemberSchema: z.ZodObject<{
433
440
  id: z.ZodUUID;
441
+ onchainId: z.ZodUUID;
434
442
  email: z.ZodEmail;
435
443
  name: z.ZodNullable<z.ZodString>;
436
444
  isAdmin: z.ZodBoolean;
@@ -1455,6 +1463,199 @@ export declare const ProposeCallRequestSchema: z.ZodObject<{
1455
1463
  name: z.ZodOptional<z.ZodString>;
1456
1464
  }, z.core.$strip>;
1457
1465
  export type ProposeCallRequest = z.infer<typeof ProposeCallRequestSchema>;
1466
+ /** An EVM transfer policy: the token and the cap per transfer, with the creator's `TransferPolicyActivation`. */
1467
+ export declare const CreateEvmTransferPolicyRequestSchema: z.ZodObject<{
1468
+ chain: z.ZodEnum<{
1469
+ ETHEREUM: "ETHEREUM";
1470
+ BASE: "BASE";
1471
+ ARBITRUM: "ARBITRUM";
1472
+ ROBINHOOD: "ROBINHOOD";
1473
+ }>;
1474
+ token: z.ZodString;
1475
+ maxAmount: z.ZodString;
1476
+ signature: z.ZodString;
1477
+ id: z.ZodUUID;
1478
+ vaultId: z.ZodUUID;
1479
+ name: z.ZodString;
1480
+ type: z.ZodEnum<{
1481
+ Contractor_Stipend: "Contractor_Stipend";
1482
+ Operational_Expense: "Operational_Expense";
1483
+ Treasury_Management: "Treasury_Management";
1484
+ }>;
1485
+ signers: z.ZodArray<z.ZodUUID>;
1486
+ threshold: z.ZodNumber;
1487
+ }, z.core.$strict>;
1488
+ /** An EVM call policy: the wrapper it binds (function `entry`), with the creator's `TransactionPolicyActivation`. */
1489
+ export declare const CreateEvmCallPolicyRequestSchema: z.ZodObject<{
1490
+ chain: z.ZodEnum<{
1491
+ ETHEREUM: "ETHEREUM";
1492
+ BASE: "BASE";
1493
+ ARBITRUM: "ARBITRUM";
1494
+ ROBINHOOD: "ROBINHOOD";
1495
+ }>;
1496
+ wrapper: z.ZodString;
1497
+ signature: z.ZodString;
1498
+ id: z.ZodUUID;
1499
+ vaultId: z.ZodUUID;
1500
+ name: z.ZodString;
1501
+ type: z.ZodEnum<{
1502
+ Contractor_Stipend: "Contractor_Stipend";
1503
+ Operational_Expense: "Operational_Expense";
1504
+ Treasury_Management: "Treasury_Management";
1505
+ }>;
1506
+ signers: z.ZodArray<z.ZodUUID>;
1507
+ threshold: z.ZodNumber;
1508
+ }, z.core.$strict>;
1509
+ /**
1510
+ * A BTC transfer policy, with the creator's approval: a Bitcoin Signed Message
1511
+ * over the activation digest (`btc-policy-activation.ts`) by their BTC key's
1512
+ * Cofund leaf.
1513
+ */
1514
+ export declare const CreateBtcPolicyRequestSchema: z.ZodObject<{
1515
+ chain: z.ZodLiteral<"BTC">;
1516
+ token: z.ZodLiteral<"BTC">;
1517
+ maxAmount: z.ZodString;
1518
+ signature: z.ZodString;
1519
+ id: z.ZodUUID;
1520
+ vaultId: z.ZodUUID;
1521
+ name: z.ZodString;
1522
+ type: z.ZodEnum<{
1523
+ Contractor_Stipend: "Contractor_Stipend";
1524
+ Operational_Expense: "Operational_Expense";
1525
+ Treasury_Management: "Treasury_Management";
1526
+ }>;
1527
+ signers: z.ZodArray<z.ZodUUID>;
1528
+ threshold: z.ZodNumber;
1529
+ }, z.core.$strict>;
1530
+ /** POST /policies (admins). Retrying with the same `id` returns the same policy. */
1531
+ export declare const CreatePolicyRequestSchema: z.ZodUnion<readonly [z.ZodObject<{
1532
+ chain: z.ZodEnum<{
1533
+ ETHEREUM: "ETHEREUM";
1534
+ BASE: "BASE";
1535
+ ARBITRUM: "ARBITRUM";
1536
+ ROBINHOOD: "ROBINHOOD";
1537
+ }>;
1538
+ token: z.ZodString;
1539
+ maxAmount: z.ZodString;
1540
+ signature: z.ZodString;
1541
+ id: z.ZodUUID;
1542
+ vaultId: z.ZodUUID;
1543
+ name: z.ZodString;
1544
+ type: z.ZodEnum<{
1545
+ Contractor_Stipend: "Contractor_Stipend";
1546
+ Operational_Expense: "Operational_Expense";
1547
+ Treasury_Management: "Treasury_Management";
1548
+ }>;
1549
+ signers: z.ZodArray<z.ZodUUID>;
1550
+ threshold: z.ZodNumber;
1551
+ }, z.core.$strict>, z.ZodObject<{
1552
+ chain: z.ZodEnum<{
1553
+ ETHEREUM: "ETHEREUM";
1554
+ BASE: "BASE";
1555
+ ARBITRUM: "ARBITRUM";
1556
+ ROBINHOOD: "ROBINHOOD";
1557
+ }>;
1558
+ wrapper: z.ZodString;
1559
+ signature: z.ZodString;
1560
+ id: z.ZodUUID;
1561
+ vaultId: z.ZodUUID;
1562
+ name: z.ZodString;
1563
+ type: z.ZodEnum<{
1564
+ Contractor_Stipend: "Contractor_Stipend";
1565
+ Operational_Expense: "Operational_Expense";
1566
+ Treasury_Management: "Treasury_Management";
1567
+ }>;
1568
+ signers: z.ZodArray<z.ZodUUID>;
1569
+ threshold: z.ZodNumber;
1570
+ }, z.core.$strict>, z.ZodObject<{
1571
+ chain: z.ZodLiteral<"BTC">;
1572
+ token: z.ZodLiteral<"BTC">;
1573
+ maxAmount: z.ZodString;
1574
+ signature: z.ZodString;
1575
+ id: z.ZodUUID;
1576
+ vaultId: z.ZodUUID;
1577
+ name: z.ZodString;
1578
+ type: z.ZodEnum<{
1579
+ Contractor_Stipend: "Contractor_Stipend";
1580
+ Operational_Expense: "Operational_Expense";
1581
+ Treasury_Management: "Treasury_Management";
1582
+ }>;
1583
+ signers: z.ZodArray<z.ZodUUID>;
1584
+ threshold: z.ZodNumber;
1585
+ }, z.core.$strict>]>;
1586
+ export type CreatePolicyRequest = z.infer<typeof CreatePolicyRequestSchema>;
1587
+ /** POST /invites/user-id (admins). */
1588
+ export declare const InviteUserIdRequestSchema: z.ZodObject<{
1589
+ email: z.ZodEmail;
1590
+ }, z.core.$strip>;
1591
+ export type InviteUserIdRequest = z.infer<typeof InviteUserIdRequestSchema>;
1592
+ /**
1593
+ * The id an invite to that email must seat: the account's own when the email
1594
+ * has one (its identity in every cofund), else null, and the invite seats its
1595
+ * own `id`.
1596
+ */
1597
+ export declare const InviteUserIdSchema: z.ZodObject<{
1598
+ userId: z.ZodNullable<z.ZodUUID>;
1599
+ }, z.core.$strip>;
1600
+ export type InviteUserId = z.infer<typeof InviteUserIdSchema>;
1601
+ /** POST /invites (admins). Retrying with the same `id` returns the same invite. */
1602
+ export declare const CreateInviteRequestSchema: z.ZodObject<{
1603
+ id: z.ZodUUID;
1604
+ email: z.ZodEmail;
1605
+ firstName: z.ZodString;
1606
+ lastName: z.ZodString;
1607
+ position: z.ZodString;
1608
+ isAdmin: z.ZodBoolean;
1609
+ userId: z.ZodUUID;
1610
+ chains: z.ZodArray<z.ZodObject<{
1611
+ chain: z.ZodEnum<{
1612
+ ETHEREUM: "ETHEREUM";
1613
+ BASE: "BASE";
1614
+ ARBITRUM: "ARBITRUM";
1615
+ ROBINHOOD: "ROBINHOOD";
1616
+ }>;
1617
+ expiresAt: z.ZodString;
1618
+ signature: z.ZodString;
1619
+ }, z.core.$strip>>;
1620
+ }, z.core.$strip>;
1621
+ export type CreateInviteRequest = z.infer<typeof CreateInviteRequestSchema>;
1622
+ /**
1623
+ * An invite as created: the seat, and its row on each chain. A row is
1624
+ * claimable (AWAITING_SUBMISSION), or an admin seat awaiting the other admins'
1625
+ * co-signatures (AWAITING_SIGNATURES), when its id is the `invite` request
1626
+ * they sign.
1627
+ */
1628
+ export declare const ApiCreatedInviteSchema: z.ZodObject<{
1629
+ id: z.ZodUUID;
1630
+ cofundId: z.ZodUUID;
1631
+ member: z.ZodObject<{
1632
+ id: z.ZodUUID;
1633
+ email: z.ZodEmail;
1634
+ isAdmin: z.ZodBoolean;
1635
+ position: z.ZodString;
1636
+ }, z.core.$strip>;
1637
+ chains: z.ZodArray<z.ZodObject<{
1638
+ chain: z.ZodEnum<{
1639
+ ETHEREUM: "ETHEREUM";
1640
+ BASE: "BASE";
1641
+ ARBITRUM: "ARBITRUM";
1642
+ ROBINHOOD: "ROBINHOOD";
1643
+ }>;
1644
+ id: z.ZodUUID;
1645
+ status: z.ZodEnum<{
1646
+ FAILED: "FAILED";
1647
+ AWAITING_SIGNATURES: "AWAITING_SIGNATURES";
1648
+ COMPLETED: "COMPLETED";
1649
+ AWAITING_SUBMISSION: "AWAITING_SUBMISSION";
1650
+ PROCESSING: "PROCESSING";
1651
+ EXPIRED: "EXPIRED";
1652
+ CANCELED: "CANCELED";
1653
+ REJECTED: "REJECTED";
1654
+ }>;
1655
+ expiresAt: z.ZodString;
1656
+ }, z.core.$strip>>;
1657
+ }, z.core.$strip>;
1658
+ export type ApiCreatedInvite = z.infer<typeof ApiCreatedInviteSchema>;
1458
1659
  /** POST /requests/{id}/signatures. A BTC policy approval is a compact signature; the rest are EIP-712. */
1459
1660
  export declare const SignatureRequestSchema: z.ZodObject<{
1460
1661
  signature: z.ZodUnion<readonly [z.ZodString, z.ZodString]>;
package/dist/api-types.js CHANGED
@@ -111,6 +111,11 @@ export const PolicyTypeSchema = z.enum([
111
111
  "Operational_Expense",
112
112
  "Treasury_Management",
113
113
  ]);
114
+ /** The types the API lists and creates: not the app's own deposit and invoice policies. */
115
+ export const ApiPolicyTypeSchema = PolicyTypeSchema.exclude([
116
+ "Crypto_Onramp",
117
+ "Business_Invoice",
118
+ ]);
114
119
  export const PolicyStatusSchema = z.enum([
115
120
  "PENDING",
116
121
  "ACTIVE",
@@ -290,11 +295,8 @@ export const ApiPolicySchema = z
290
295
  .object({
291
296
  id: Uuid,
292
297
  name: PolicyName,
293
- /**
294
- * The `messageType` every request under it signs. The app's own deposit
295
- * and invoice policies are not listed, as in the app.
296
- */
297
- type: PolicyTypeSchema.exclude(["Crypto_Onramp", "Business_Invoice"]),
298
+ /** The `messageType` every request under it signs. */
299
+ type: ApiPolicyTypeSchema,
298
300
  status: PolicyStatusSchema,
299
301
  threshold: Threshold,
300
302
  vault: ApiVaultSchema,
@@ -312,6 +314,8 @@ export const ApiPolicySchema = z
312
314
  recipients.every((recipient) => BtcAddress.safeParse(recipient).success), "A BTC policy's recipients are Bitcoin addresses");
313
315
  export const ApiMemberSchema = z.object({
314
316
  id: Uuid,
317
+ /** The id the member is registered under on chain here: what an EVM policy activation names. */
318
+ onchainId: Uuid,
315
319
  email: z.email(),
316
320
  name: PersonName.nullable(),
317
321
  isAdmin: z.boolean(),
@@ -524,6 +528,121 @@ export const ProposeCallRequestSchema = z.object({
524
528
  signature: Signature,
525
529
  name: RequestName.optional(),
526
530
  });
531
+ /** A policy's cap per transfer: base units, at most 30 digits, as the app stores it. */
532
+ const PolicyCap = PositiveBaseUnits.max(30, "at most 30 digits");
533
+ const CreatePolicyCommon = {
534
+ /** A UUID the caller picks: the policy's id, and the `policyId` and `authId` its activation signs. */
535
+ id: Uuid,
536
+ vaultId: Uuid,
537
+ name: PolicyName,
538
+ type: ApiPolicyTypeSchema,
539
+ /** Members' ids. The activation names each one's `onchainId` (EVM) or account xpub (BTC). */
540
+ signers: z
541
+ .array(Uuid)
542
+ .min(1)
543
+ .max(100)
544
+ .refine((ids) => distinct(ids, (id) => id), "Each signer once"),
545
+ threshold: Threshold,
546
+ };
547
+ /** An EVM transfer policy: the token and the cap per transfer, with the creator's `TransferPolicyActivation`. */
548
+ export const CreateEvmTransferPolicyRequestSchema = z
549
+ .object({
550
+ ...CreatePolicyCommon,
551
+ chain: EvmChainNameSchema,
552
+ token: Address,
553
+ maxAmount: PolicyCap,
554
+ signature: Signature,
555
+ })
556
+ .strict();
557
+ /** An EVM call policy: the wrapper it binds (function `entry`), with the creator's `TransactionPolicyActivation`. */
558
+ export const CreateEvmCallPolicyRequestSchema = z
559
+ .object({
560
+ ...CreatePolicyCommon,
561
+ chain: EvmChainNameSchema,
562
+ wrapper: Address,
563
+ signature: Signature,
564
+ })
565
+ .strict();
566
+ /**
567
+ * A BTC transfer policy, with the creator's approval: a Bitcoin Signed Message
568
+ * over the activation digest (`btc-policy-activation.ts`) by their BTC key's
569
+ * Cofund leaf.
570
+ */
571
+ export const CreateBtcPolicyRequestSchema = z
572
+ .object({
573
+ ...CreatePolicyCommon,
574
+ chain: z.literal("BTC"),
575
+ token: z.literal("BTC"),
576
+ maxAmount: PolicyCap,
577
+ signature: BtcMessageSignature,
578
+ })
579
+ .strict();
580
+ /** POST /policies (admins). Retrying with the same `id` returns the same policy. */
581
+ export const CreatePolicyRequestSchema = z
582
+ .union([
583
+ CreateEvmTransferPolicyRequestSchema,
584
+ CreateEvmCallPolicyRequestSchema,
585
+ CreateBtcPolicyRequestSchema,
586
+ ])
587
+ .refine(({ threshold, signers }) => threshold <= signers.length, {
588
+ message: "The threshold is more than the signers",
589
+ path: ["threshold"],
590
+ });
591
+ /** A first or last name, as the invite form takes it. */
592
+ const NamePart = z.string().min(2).max(100);
593
+ const EmailAddress = z.email().max(254);
594
+ /** POST /invites/user-id (admins). */
595
+ export const InviteUserIdRequestSchema = z.object({ email: EmailAddress });
596
+ /**
597
+ * The id an invite to that email must seat: the account's own when the email
598
+ * has one (its identity in every cofund), else null, and the invite seats its
599
+ * own `id`.
600
+ */
601
+ export const InviteUserIdSchema = z.object({ userId: Uuid.nullable() });
602
+ /** POST /invites (admins). Retrying with the same `id` returns the same invite. */
603
+ export const CreateInviteRequestSchema = z.object({
604
+ /** A UUID the caller picks: the invite's id, and the `authId` every chain's `InviteUser` signs. */
605
+ id: Uuid,
606
+ email: EmailAddress,
607
+ firstName: NamePart,
608
+ lastName: NamePart,
609
+ /** The seat's position, signed as `userPosition`. */
610
+ position: Position.min(2),
611
+ isAdmin: z.boolean(),
612
+ /** The `userId` signed: POST /invites/user-id's answer, else `id`. */
613
+ userId: Uuid,
614
+ /** The inviting admin's `InviteUser` on each EVM chain. */
615
+ chains: z
616
+ .array(z.object({
617
+ chain: EvmChainNameSchema,
618
+ expiresAt: UnixSeconds,
619
+ signature: Signature,
620
+ }))
621
+ .min(1)
622
+ .refine((chains) => distinct(chains, ({ chain }) => chain), "One signature per chain"),
623
+ });
624
+ /**
625
+ * An invite as created: the seat, and its row on each chain. A row is
626
+ * claimable (AWAITING_SUBMISSION), or an admin seat awaiting the other admins'
627
+ * co-signatures (AWAITING_SIGNATURES), when its id is the `invite` request
628
+ * they sign.
629
+ */
630
+ export const ApiCreatedInviteSchema = z.object({
631
+ id: Uuid,
632
+ cofundId: Uuid,
633
+ member: z.object({
634
+ id: Uuid,
635
+ email: z.email(),
636
+ isAdmin: z.boolean(),
637
+ position: Position,
638
+ }),
639
+ chains: z.array(z.object({
640
+ chain: EvmChainNameSchema,
641
+ id: Uuid,
642
+ status: InviteStatusSchema,
643
+ expiresAt: UnixSeconds,
644
+ })),
645
+ });
527
646
  /** POST /requests/{id}/signatures. A BTC policy approval is a compact signature; the rest are EIP-712. */
528
647
  export const SignatureRequestSchema = z.object({
529
648
  signature: z.union([Signature, BtcMessageSignature]),
@@ -10,7 +10,7 @@ export type CliIo = {
10
10
  /** Ask a yes/no question; absent when nobody can answer (no TTY). */
11
11
  confirm?: (question: string) => Promise<boolean>;
12
12
  };
13
- export declare const USAGE = "usage:\n cofund --help\n cofund keys init [--force]\n cofund me\n cofund invite accept [--invite <id>] [--company <name>]\n cofund requests [list] [--cofund <id>]\n cofund requests show <id> [--cofund <id>]\n cofund requests sign <id> [--yes] [--cofund <id>]\n cofund requests reject <id> [--yes] [--cofund <id>]\n cofund stocks buy <SYMBOL> --spend <USDG> [--policy <id>] [--cofund <id>]\n cofund webhooks [list]\n cofund webhooks add <url> [--events a,b]\n cofund webhooks remove <id>";
13
+ export declare const USAGE = "usage:\n cofund --help\n cofund keys init [--force]\n cofund me\n cofund invite accept [--invite <id>] [--company <name>]\n cofund invite create --email <email> --first-name <name> --last-name <name>\n --position <title> [--admin] [--chains A,B] [--yes] [--cofund <id>]\n cofund policies create --vault <id> --name <name> --signers <id|email,...>\n --threshold <n> (--token <address|BTC> --max-amount <base units> |\n --wrapper <address>) [--type <PolicyType>] [--yes] [--cofund <id>]\n cofund requests [list] [--cofund <id>]\n cofund requests show <id> [--cofund <id>]\n cofund requests sign <id> [--yes] [--cofund <id>]\n cofund requests reject <id> [--yes] [--cofund <id>]\n cofund stocks buy <SYMBOL> --spend <USDG> [--policy <id>] [--cofund <id>]\n cofund webhooks [list]\n cofund webhooks add <url> [--events a,b]\n cofund webhooks remove <id>";
14
14
  export declare function keystorePath(io: CliIo, flag?: string): string;
15
15
  export declare function client(io: CliIo): CofundClient;
16
16
  export declare function chainPins(io: CliIo): ChainPins;
@@ -7,6 +7,11 @@ export const USAGE = `usage:
7
7
  cofund keys init [--force]
8
8
  cofund me
9
9
  cofund invite accept [--invite <id>] [--company <name>]
10
+ cofund invite create --email <email> --first-name <name> --last-name <name>
11
+ --position <title> [--admin] [--chains A,B] [--yes] [--cofund <id>]
12
+ cofund policies create --vault <id> --name <name> --signers <id|email,...>
13
+ --threshold <n> (--token <address|BTC> --max-amount <base units> |
14
+ --wrapper <address>) [--type <PolicyType>] [--yes] [--cofund <id>]
10
15
  cofund requests [list] [--cofund <id>]
11
16
  cofund requests show <id> [--cofund <id>]
12
17
  cofund requests sign <id> [--yes] [--cofund <id>]
@@ -1,6 +1,15 @@
1
- import { type CliIo } from "./context.ts";
1
+ import { type CliIo, type CofundValues } from "./context.ts";
2
2
  export declare function inviteAccept(io: CliIo, values: {
3
3
  keystore?: string;
4
4
  invite?: string;
5
5
  company?: string;
6
6
  }): Promise<number>;
7
+ export type InviteCreateValues = CofundValues & {
8
+ email?: string;
9
+ "first-name"?: string;
10
+ "last-name"?: string;
11
+ position?: string;
12
+ admin?: boolean;
13
+ chains?: string;
14
+ };
15
+ export declare function inviteCreate(io: CliIo, values: InviteCreateValues): Promise<number>;
@@ -1,7 +1,13 @@
1
- import { checkInvite, signInviteAcceptance } from "../invite.js";
1
+ import { randomUUID } from "node:crypto";
2
+ import { EvmChainNameSchema, } from "../api-types.js";
3
+ import { checkInvite, signInviteAcceptance, signInviteCreation, } from "../invite.js";
2
4
  import { btcOwnershipProof, evmWallet, readKeystore, writeKeystore, } from "../keys.js";
3
- import { chainPins, client, keystorePath } from "./context.js";
4
- /** `cofund invite accept`: check the invite, pin the company, sign with the local keys. */
5
+ import { approval, chainPins, client, keystorePath, pickCofund, pinnedCompany, USAGE, } from "./context.js";
6
+ /**
7
+ * `cofund invite accept`: check the invite, pin the company, sign with the
8
+ * local keys. `cofund invite create`: an admin invites a member, signing each
9
+ * chain's seat with the local key.
10
+ */
5
11
  function inviteLines(invite) {
6
12
  const [first] = invite.chains;
7
13
  const expires = new Date(Number(first.message.expiresAt) * 1000);
@@ -85,3 +91,75 @@ export async function inviteAccept(io, values) {
85
91
  });
86
92
  return 0;
87
93
  }
94
+ /** --chains, or every EVM chain the cofund has a vault on. */
95
+ async function inviteChains(api, cofundId, flag) {
96
+ if (flag) {
97
+ return flag.split(",").map((chain) => EvmChainNameSchema.parse(chain));
98
+ }
99
+ const chains = new Set();
100
+ for (const { chain } of await api.vaults(cofundId)) {
101
+ if (chain !== "BTC") {
102
+ chains.add(chain);
103
+ }
104
+ }
105
+ return [...chains];
106
+ }
107
+ function newInviteLines(invite, company) {
108
+ return [
109
+ `invite ${invite.firstName} ${invite.lastName} <${invite.email}>`,
110
+ `seat ${invite.isAdmin ? "admin" : "member"}, ${invite.position}`,
111
+ `chains ${invite.chains.join(", ")}`,
112
+ `company "${company}"`,
113
+ ];
114
+ }
115
+ export async function inviteCreate(io, values) {
116
+ const { email, position } = values;
117
+ const firstName = values["first-name"];
118
+ const lastName = values["last-name"];
119
+ if (!(email && firstName && lastName && position)) {
120
+ io.err(USAGE);
121
+ return 1;
122
+ }
123
+ const keystore = readKeystore(keystorePath(io, values.keystore));
124
+ const api = client(io);
125
+ const me = await api.me();
126
+ const cofundId = pickCofund(me, values.cofund);
127
+ const company = pinnedCompany(keystore, cofundId);
128
+ const chains = await inviteChains(api, cofundId, values.chains);
129
+ if (chains.length === 0) {
130
+ io.err("The cofund has no EVM vault to seat a member on");
131
+ return 1;
132
+ }
133
+ const id = randomUUID();
134
+ const { userId } = await api.inviteUserId(cofundId, email);
135
+ const invite = {
136
+ id,
137
+ callerId: me.id,
138
+ userId: userId ?? id,
139
+ email,
140
+ firstName,
141
+ lastName,
142
+ position,
143
+ isAdmin: Boolean(values.admin),
144
+ chains,
145
+ };
146
+ for (const line of newInviteLines(invite, company)) {
147
+ io.out(line);
148
+ }
149
+ const approve = approval(io, values.yes, "Sign and send this invite?");
150
+ if (!approve) {
151
+ io.err("Pass --yes to invite without a prompt");
152
+ return 1;
153
+ }
154
+ if (!(await approve())) {
155
+ io.err("Not invited");
156
+ return 1;
157
+ }
158
+ const body = await signInviteCreation(invite, company, chainPins(io), evmWallet(keystore));
159
+ const created = await api.createInvite(cofundId, body);
160
+ io.out(`invited ${created.member.email} (${created.id})`);
161
+ for (const row of created.chains) {
162
+ io.out(`${row.chain.padEnd(9)} ${row.id} ${row.status}`);
163
+ }
164
+ return 0;
165
+ }
@@ -0,0 +1,17 @@
1
+ import { type CliIo, type CofundValues } from "./context.ts";
2
+ export type PolicyValues = CofundValues & {
3
+ vault?: string;
4
+ name?: string;
5
+ type?: string;
6
+ signers?: string;
7
+ threshold?: string;
8
+ token?: string;
9
+ "max-amount"?: string;
10
+ wrapper?: string;
11
+ };
12
+ /**
13
+ * `cofund policies create`: build the policy's activation from the flags, sign
14
+ * it with the local key and create the policy. It activates at once when one
15
+ * admin may activate it, else it awaits the other admins.
16
+ */
17
+ export declare function policiesCreate(io: CliIo, values: PolicyValues): Promise<number>;