@cardstack/boxel-cli 0.12.0-unstable.12 → 0.12.0-unstable.14
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/bundled-test-harness/assets/{ai-module-creation-test-D9FuBmi8.js → ai-module-creation-test-BuOdKEfn.js} +1 -1
- package/bundled-test-harness/assets/{app-B6ALqYQD.js → app-DFQ4JqlP.js} +2 -2
- package/bundled-test-harness/assets/{binary-upload-dedupe-test-eI1yGxdL.js → binary-upload-dedupe-test-jbrJQUz0.js} +1 -1
- package/bundled-test-harness/assets/{can-invoke-test-5enpsRYF.js → can-invoke-test-DoWzbZ8k.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-paging-test-CdtsXIpu.js → card-chooser-paging-test-VRmsX6sY.js} +1 -1
- package/bundled-test-harness/assets/{card-chooser-test-CpUK3_Su.js → card-chooser-test-CgrV_vWi.js} +1 -1
- package/bundled-test-harness/assets/{card-def-field-def-relationships-test-Z1O-t73y.js → card-def-field-def-relationships-test-BHQoFm4_.js} +1 -1
- package/bundled-test-harness/assets/{card-delete-test-DI16JAzu.js → card-delete-test-DCmoRd03.js} +1 -1
- package/bundled-test-harness/assets/{codeblocks-test-DEaQeMcn.js → codeblocks-test-DQU6dnQH.js} +1 -1
- package/bundled-test-harness/assets/{compaction-test-DliwzHTd.js → compaction-test-B7JuwjQ7.js} +1 -1
- package/bundled-test-harness/assets/{container-BHHKErdI.js → container-CwRfjGxi.js} +1 -1
- package/bundled-test-harness/assets/{create-listing-modal-test-BZiD6UL9.js → create-listing-modal-test-CxkuYwnv.js} +1 -1
- package/bundled-test-harness/assets/{debug-message-test-D7VEbny3.js → debug-message-test-CyrcsddQ.js} +1 -1
- package/bundled-test-harness/assets/{fallback-banner-live-delete-test-DwXohU1S.js → fallback-banner-live-delete-test-Bxxt9EBl.js} +1 -1
- package/bundled-test-harness/assets/{fallback-banner-test-jEmNTOeh.js → fallback-banner-test-BzmDOq0F.js} +1 -1
- package/bundled-test-harness/assets/{fallback-models-test-BfgkQJ8l.js → fallback-models-test-mZvUg3zI.js} +1 -1
- package/bundled-test-harness/assets/{file-attachment-test-CaX44AfG.js → file-attachment-test-ybs1HRMC.js} +1 -1
- package/bundled-test-harness/assets/{general-test-zz1QVTIv.js → general-test-jB4EQTh8.js} +1 -1
- package/bundled-test-harness/assets/{main-CYiHxOxo.js → main-uvgWNg1U.js} +1 -1
- package/bundled-test-harness/assets/{model-cost-tier-test-DcEeQJwF.js → model-cost-tier-test-BPCl0EeO.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-card-chooser-test-DYSf_9jJ.js → operator-mode-card-chooser-test-D8RfBsEc.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-links-test-BA3gjmzY.js → operator-mode-links-test-DmpvnpHI.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-test-C6iSqlfM.js → operator-mode-test-BeCWE8qH.js} +1 -1
- package/bundled-test-harness/assets/{operator-mode-ui-test-BFGw2suP.js → operator-mode-ui-test-D0k0pr1Z.js} +1 -1
- package/bundled-test-harness/assets/{overlay-menu-items-test-Ch351wNL.js → overlay-menu-items-test-l4-oOOi5.js} +1 -1
- package/bundled-test-harness/assets/{panel-B8BaMllL.js → panel-8nNZbvzg.js} +16 -4
- package/bundled-test-harness/assets/{past-sessions-test-DoVlENnJ.js → past-sessions-test-DrJ_h58w.js} +1 -1
- package/bundled-test-harness/assets/{permission-checking-realm-test-BrBeo81O.js → permission-checking-realm-test-CL_nySH1.js} +1 -1
- package/bundled-test-harness/assets/{quick-look-test-InFXyTqv.js → quick-look-test-DMfAGvgd.js} +1 -1
- package/bundled-test-harness/assets/{realm-api-keys-test-Bgg-gi56.js → realm-api-keys-test-DVkuepc8.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-policy-standing-test-BIEwUQYA.js → realm-config-policy-standing-test-DXS8kYVC.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-CDj8rdvZ.js → realm-config-routing-rule-edit-test-CPTifvXx.js} +1 -1
- package/bundled-test-harness/assets/{realm-config-settings-test-Csi-H8bJ.js → realm-config-settings-test-Civ3ZVxT.js} +1 -1
- package/bundled-test-harness/assets/{reasoning-test-MNcXjMV3.js → reasoning-test-DYSQdtDZ.js} +1 -1
- package/bundled-test-harness/assets/{room-message-test-DxMyOu8L.js → room-message-test-CSbclniG.js} +1 -1
- package/bundled-test-harness/assets/{run-realm-code-test-DTXPkVu_.js → run-realm-code-test-B9KJIaYx.js} +81 -0
- package/bundled-test-harness/assets/{scrolling-test-6-Soq39G.js → scrolling-test-nDHEhgzp.js} +2 -2
- package/bundled-test-harness/assets/{sending-test-DXOw4MlE.js → sending-test-CjrOIeGV.js} +1 -1
- package/bundled-test-harness/assets/{skills-test-BiUE1x5l.js → skills-test-Be4crUyO.js} +1 -1
- package/bundled-test-harness/assets/{store-test-Cbf2xZ40.js → store-test-BCyLVXZY.js} +1 -1
- package/bundled-test-harness/assets/{tests-Blos5s0A.js → tests-DbU4zMsp.js} +43 -43
- package/bundled-test-harness/assets/{text-input-validator-test-BxG8Dc2i.js → text-input-validator-test-DZuVdEKj.js} +1 -1
- package/bundled-test-harness/assets/{to-device-streaming-test-CGuKaubC.js → to-device-streaming-test-C3i5WmnX.js} +1 -1
- package/bundled-test-harness/assets/{token-usage-test-DJJmXyD8.js → token-usage-test-1AWgVaXO.js} +1 -1
- package/bundled-test-harness/assets/{tools-test-DxDvBhPf.js → tools-test-BSmxEyfO.js} +1 -1
- package/bundled-test-harness/assets/{turn-settling-test-Co2Dmnzm.js → turn-settling-test-S3ROiZu9.js} +1 -1
- package/bundled-test-harness/index.html +5 -5
- package/bundled-test-harness/tests/index.html +5 -5
- package/bundled-types/host-app/lib/secure-sign-in.ts +5 -3
- package/bundled-types/host-app/services/matrix-service.ts +3 -2
- package/bundled-types/host-app/services/tool-service.ts +36 -5
- package/dist/index.js +290 -290
- package/package.json +1 -1
- package/src/commands/keys/approve.ts +2 -1
- package/src/commands/keys/index.ts +48 -3
- package/src/commands/keys/manage.ts +257 -21
- package/src/lib/key-bindings.ts +2 -1
- package/src/lib/user-keys.ts +174 -4
package/package.json
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type WindowOutcome,
|
|
26
26
|
} from '../../lib/sso-login.ts';
|
|
27
27
|
import {
|
|
28
|
+
anonymousCapsClause,
|
|
28
29
|
findUserKey,
|
|
29
30
|
listOwnShares,
|
|
30
31
|
type UserKeyGrant,
|
|
@@ -372,7 +373,7 @@ function printBatch(
|
|
|
372
373
|
say(
|
|
373
374
|
ctx,
|
|
374
375
|
share.anonymous
|
|
375
|
-
? ` ${FG_YELLOW}Anyone on the internet who opens this card, including people who aren't signed in, can make ${operations.join(', ')} with ${share.label}${share
|
|
376
|
+
? ` ${FG_YELLOW}Anyone on the internet who opens this card, including people who aren't signed in, can make ${operations.join(', ')} with ${share.label}${anonymousCapsClause(share)}.${RESET}`
|
|
376
377
|
: ` This realm lets people who aren't signed in make ${operations.join(', ')}, but ${share.label}'s key allows signed-in people only, so they're asked to sign in.`,
|
|
377
378
|
);
|
|
378
379
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
keysClients,
|
|
13
13
|
keysConnect,
|
|
14
14
|
keysDelete,
|
|
15
|
+
keysLimits,
|
|
15
16
|
keysList,
|
|
16
17
|
keysReplace,
|
|
17
18
|
keysShare,
|
|
@@ -19,8 +20,10 @@ import {
|
|
|
19
20
|
keysShow,
|
|
20
21
|
keysSwap,
|
|
21
22
|
keysUnshare,
|
|
23
|
+
keysUsage,
|
|
22
24
|
runKeysCommand,
|
|
23
25
|
signInToManageKeys,
|
|
26
|
+
type LimitsOptions,
|
|
24
27
|
} from './manage.ts';
|
|
25
28
|
import {
|
|
26
29
|
keysApprove,
|
|
@@ -175,12 +178,17 @@ export function registerKeysCommand(program: Command): void {
|
|
|
175
178
|
'--anonymous-cap <calls>',
|
|
176
179
|
"With --anonymous, the most calls a day people who aren't signed in make with it, all together, in every realm it's shared into",
|
|
177
180
|
)
|
|
181
|
+
.option(
|
|
182
|
+
'--anonymous-monthly-cap <calls>',
|
|
183
|
+
"With --anonymous, the most calls a UTC month people who aren't signed in make with it, all together, in every realm it's shared into",
|
|
184
|
+
)
|
|
178
185
|
.action(
|
|
179
186
|
(
|
|
180
187
|
name: string,
|
|
181
188
|
options: ValueCommandOptions & {
|
|
182
189
|
anonymous?: boolean;
|
|
183
190
|
anonymousCap?: string;
|
|
191
|
+
anonymousMonthlyCap?: string;
|
|
184
192
|
},
|
|
185
193
|
) => runKeysCommand(options.json, () => keysAdd(name, options)),
|
|
186
194
|
);
|
|
@@ -188,20 +196,24 @@ export function registerKeysCommand(program: Command): void {
|
|
|
188
196
|
keys
|
|
189
197
|
.command('anonymous')
|
|
190
198
|
.description(
|
|
191
|
-
"Say whether people who aren't signed in may use one of your keys, wherever a realm you share it into lets them make the call. off clears
|
|
199
|
+
"Say whether people who aren't signed in may use one of your keys, wherever a realm you share it into lets them make the call. off clears both caps; on keeps a cap you don't name",
|
|
192
200
|
)
|
|
193
201
|
.argument('<name>', 'The key')
|
|
194
202
|
.argument('<on|off>', 'Allow it or not')
|
|
195
203
|
.option(
|
|
196
204
|
'--cap <calls>',
|
|
197
|
-
"With on, the most calls a day people who aren't signed in make with it, all together, in every realm it's shared into",
|
|
205
|
+
"With on, the most calls a day people who aren't signed in make with it, all together, in every realm it's shared into, or none to remove the cap",
|
|
206
|
+
)
|
|
207
|
+
.option(
|
|
208
|
+
'--monthly-cap <calls>',
|
|
209
|
+
"With on, the most calls a UTC month people who aren't signed in make with it, all together, in every realm it's shared into, or none to remove the cap",
|
|
198
210
|
)
|
|
199
211
|
.option('--json', 'Output JSON')
|
|
200
212
|
.action(
|
|
201
213
|
(
|
|
202
214
|
name: string,
|
|
203
215
|
setting: string,
|
|
204
|
-
options: { cap?: string; json?: boolean },
|
|
216
|
+
options: { cap?: string; monthlyCap?: string; json?: boolean },
|
|
205
217
|
) =>
|
|
206
218
|
runKeysCommand(options.json, () =>
|
|
207
219
|
keysAnonymous(name, setting, options),
|
|
@@ -366,6 +378,39 @@ export function registerKeysCommand(program: Command): void {
|
|
|
366
378
|
runKeysCommand(options.json, () => keysShares(options)),
|
|
367
379
|
);
|
|
368
380
|
|
|
381
|
+
keys
|
|
382
|
+
.command('limits')
|
|
383
|
+
.description(
|
|
384
|
+
"Show or set the limits on how much of one of your keys calls from cards, the AI assistant and the CLI may spend: on a personal key, or with --realm on your share of one there. Each limit takes a number, or default for the platform's. Without a limit to change, shows the limits in force",
|
|
385
|
+
)
|
|
386
|
+
.argument('<name>', 'Your key, or with --realm the label of the share')
|
|
387
|
+
.option('--realm <url>', 'The realm the share is in')
|
|
388
|
+
.option(
|
|
389
|
+
'--rate <burst>/<refill>',
|
|
390
|
+
'Per caller: how many calls at once, and how many a minute refill, such as 25/15 (default for both puts them back)',
|
|
391
|
+
)
|
|
392
|
+
.option('--daily <n>', 'Calls a UTC day, across every caller')
|
|
393
|
+
.option(
|
|
394
|
+
'--monthly <n>',
|
|
395
|
+
'Calls a UTC month, across every caller (default: 30 daily budgets)',
|
|
396
|
+
)
|
|
397
|
+
.option('--json', 'Output JSON')
|
|
398
|
+
.action((name: string, options: LimitsOptions) =>
|
|
399
|
+
runKeysCommand(options.json, () => keysLimits(name, options)),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
keys
|
|
403
|
+
.command('usage')
|
|
404
|
+
.description(
|
|
405
|
+
"Show what one of your keys has served: this UTC day's and month's calls against its budgets, its last use, top callers, refusals by reason and the vendor's answers. With --realm, a share in that realm, for any of the realm's direct writers",
|
|
406
|
+
)
|
|
407
|
+
.argument('<name>', 'Your key, or with --realm the label of the share')
|
|
408
|
+
.option('--realm <url>', 'The realm the share is in')
|
|
409
|
+
.option('--json', 'Output JSON')
|
|
410
|
+
.action((name: string, options: { realm?: string; json?: boolean }) =>
|
|
411
|
+
runKeysCommand(options.json, () => keysUsage(name, options)),
|
|
412
|
+
);
|
|
413
|
+
|
|
369
414
|
keys
|
|
370
415
|
.command('clients')
|
|
371
416
|
.description(
|
|
@@ -29,21 +29,28 @@ import {
|
|
|
29
29
|
addUserKey,
|
|
30
30
|
canonicalRealmUrl,
|
|
31
31
|
deleteUserKey,
|
|
32
|
+
findShare,
|
|
32
33
|
findUserKey,
|
|
34
|
+
getKeyUsage,
|
|
33
35
|
listConnectableClients,
|
|
34
36
|
listOwnShares,
|
|
35
37
|
listRealmShares,
|
|
36
38
|
listUserKeys,
|
|
37
39
|
realmOpensToAnonymous,
|
|
38
40
|
replaceUserKey,
|
|
41
|
+
setKeyLimits,
|
|
39
42
|
setUserKeyAnonymousUse,
|
|
43
|
+
anonymousCapsClause,
|
|
40
44
|
shareUserKey,
|
|
41
45
|
completeOAuthConnect,
|
|
42
46
|
startOAuthConnect,
|
|
43
47
|
swapKeyIntoShare,
|
|
44
48
|
unshareUserKey,
|
|
45
49
|
type ConnectableClient,
|
|
50
|
+
type KeyLimitChanges,
|
|
51
|
+
type KeyLimits,
|
|
46
52
|
type KeyShare,
|
|
53
|
+
type KeyUsage,
|
|
47
54
|
type OAuthClientSettings,
|
|
48
55
|
type OAuthConnectOutcome,
|
|
49
56
|
type ServiceAccountDetails,
|
|
@@ -55,12 +62,14 @@ import { finishInSecureWindow, windowTimeout } from './approve.ts';
|
|
|
55
62
|
// put them into realms. Keys belong to the user, so only the share commands
|
|
56
63
|
// name a realm.
|
|
57
64
|
//
|
|
58
|
-
// Reading (`list`, `show`, `shares`, `clients`
|
|
59
|
-
// profile's ordinary realm-server
|
|
60
|
-
// a key (`add`, `replace`,
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
65
|
+
// Reading (`list`, `show`, `shares`, `clients`, `usage`, and `limits` with
|
|
66
|
+
// nothing to change) and unsharing use the profile's ordinary realm-server
|
|
67
|
+
// session. A change that stores or hands out a key (`add`, `replace`,
|
|
68
|
+
// `delete`, `share`, `swap`, `connect`) takes a key write ticket for that
|
|
69
|
+
// change, which only a session signed in on the secure origin can mint, so
|
|
70
|
+
// those commands first make sure the profile holds one, opening the secure
|
|
71
|
+
// window's sign-in in the browser when it doesn't. Changing a key's limits
|
|
72
|
+
// takes that session too, with no ticket.
|
|
64
73
|
//
|
|
65
74
|
// A key's value is typed at a masked prompt or read with `--stdin` or
|
|
66
75
|
// `--key-file`, never taken as an argument, and nothing prints one. A refusal
|
|
@@ -597,6 +606,7 @@ export async function keysAdd(
|
|
|
597
606
|
json?: boolean;
|
|
598
607
|
anonymous?: boolean;
|
|
599
608
|
anonymousCap?: string;
|
|
609
|
+
anonymousMonthlyCap?: string;
|
|
600
610
|
} & KeyValueSource &
|
|
601
611
|
ServiceAccountOptions &
|
|
602
612
|
OAuthClientOptions &
|
|
@@ -604,10 +614,13 @@ export async function keysAdd(
|
|
|
604
614
|
): Promise<UserKeyMetadata> {
|
|
605
615
|
let ctx = context(options.json, options);
|
|
606
616
|
let kind = options.kind ?? 'static';
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
options.anonymousCap
|
|
610
|
-
|
|
617
|
+
// A new key has no cap to remove, so these take numbers only.
|
|
618
|
+
let anonymousDailyCap =
|
|
619
|
+
anonymousCapOption(options.anonymous, options.anonymousCap) ?? undefined;
|
|
620
|
+
let anonymousMonthlyCap =
|
|
621
|
+
anonymousCapOption(options.anonymous, options.anonymousMonthlyCap, {
|
|
622
|
+
flag: '--anonymous-monthly-cap',
|
|
623
|
+
}) ?? undefined;
|
|
611
624
|
if (options.anonymous && kind === 'oauth-client') {
|
|
612
625
|
throw new Error(
|
|
613
626
|
"--anonymous isn't taken when adding an OAuth client: a sign-in app's people each connect their own account, so it never serves people who aren't signed in. For one that gets its own tokens (client_credentials), allow it after adding: `boxel keys anonymous <name> on`",
|
|
@@ -628,6 +641,7 @@ export async function keysAdd(
|
|
|
628
641
|
...oauthClientSettings(kind, options),
|
|
629
642
|
...(options.anonymous ? { anonymous: true } : {}),
|
|
630
643
|
...(anonymousDailyCap !== undefined ? { anonymousDailyCap } : {}),
|
|
644
|
+
...(anonymousMonthlyCap !== undefined ? { anonymousMonthlyCap } : {}),
|
|
631
645
|
});
|
|
632
646
|
emit(ctx, key, () => {
|
|
633
647
|
if (kind === 'oauth-client' && options.grantType !== 'client_credentials') {
|
|
@@ -818,31 +832,42 @@ async function warnWhereVisitorsWillUse(
|
|
|
818
832
|
}
|
|
819
833
|
if (!key.anonymous) {
|
|
820
834
|
throw new Error(
|
|
821
|
-
`${realm} lets people who aren't signed in make calls that send a shared key, so ${doing} ${name} there means they will use it, and ${name} doesn't allow them. To put it there, allow it for the key first: \`boxel keys anonymous ${name} on\` (with an optional --cap <calls>)`,
|
|
835
|
+
`${realm} lets people who aren't signed in make calls that send a shared key, so ${doing} ${name} there means they will use it, and ${name} doesn't allow them. To put it there, allow it for the key first: \`boxel keys anonymous ${name} on\` (with an optional --cap <calls> a day and --monthly-cap <calls> a month)`,
|
|
822
836
|
);
|
|
823
837
|
}
|
|
824
838
|
console.error(
|
|
825
|
-
`${FG_YELLOW}Warning: ${doing} ${name} into ${realm} means people who aren't signed in will use it. The realm's policy lets anyone on the internet who opens its cards make calls that send a shared key, and read the responses. Their calls count against your quota and/or are billed to your account with the service${key
|
|
839
|
+
`${FG_YELLOW}Warning: ${doing} ${name} into ${realm} means people who aren't signed in will use it. The realm's policy lets anyone on the internet who opens its cards make calls that send a shared key, and read the responses. Their calls count against your quota and/or are billed to your account with the service${anonymousCapsClause(key)}.${RESET}`,
|
|
826
840
|
);
|
|
827
841
|
return true;
|
|
828
842
|
}
|
|
829
843
|
|
|
830
|
-
// `--anonymous-cap
|
|
831
|
-
//
|
|
844
|
+
// An anonymous cap (`--anonymous-cap`, `--anonymous-monthly-cap`, or `--cap`
|
|
845
|
+
// and `--monthly-cap` with `on`), given only with `--anonymous` (or `on`): a
|
|
846
|
+
// whole number of calls, from 1. With `on`, `none` removes that cap (null)
|
|
847
|
+
// and keeps anonymous use on.
|
|
832
848
|
function anonymousCapOption(
|
|
833
849
|
anonymous: boolean | undefined,
|
|
834
850
|
cap: string | undefined,
|
|
835
|
-
{
|
|
836
|
-
|
|
851
|
+
{
|
|
852
|
+
flag = '--anonymous-cap',
|
|
853
|
+
with: withFlag = '--anonymous',
|
|
854
|
+
removable = false,
|
|
855
|
+
} = {},
|
|
856
|
+
): number | null | undefined {
|
|
837
857
|
if (cap === undefined) {
|
|
838
858
|
return undefined;
|
|
839
859
|
}
|
|
840
860
|
if (!anonymous) {
|
|
841
861
|
throw new Error(`${flag} is given with ${withFlag}`);
|
|
842
862
|
}
|
|
863
|
+
if (removable && cap.trim().toLowerCase() === 'none') {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
843
866
|
let calls = Number(cap);
|
|
844
867
|
if (!/^\d+$/.test(cap.trim()) || calls < 1 || calls > 2_147_483_647) {
|
|
845
|
-
throw new Error(
|
|
868
|
+
throw new Error(
|
|
869
|
+
`${flag} is a whole number of calls, from 1 to 2147483647${removable ? ', or none' : ''}`,
|
|
870
|
+
);
|
|
846
871
|
}
|
|
847
872
|
return calls;
|
|
848
873
|
}
|
|
@@ -850,18 +875,22 @@ function anonymousCapOption(
|
|
|
850
875
|
// How a key's setting for people who aren't signed in reads.
|
|
851
876
|
function anonymousUseOf(key: UserKeyMetadata): string {
|
|
852
877
|
return key.anonymous
|
|
853
|
-
? `allowed${key
|
|
878
|
+
? `allowed${anonymousCapsClause(key)}, wherever a realm's policy lets them make the call`
|
|
854
879
|
: 'not allowed';
|
|
855
880
|
}
|
|
856
881
|
|
|
857
882
|
// Whether people who aren't signed in may use one of the user's keys: its
|
|
858
883
|
// owner's setting for the key itself, which every realm it is shared into
|
|
859
|
-
// follows where that realm's policy lets them make the call. `off` clears
|
|
860
|
-
// cap
|
|
884
|
+
// follows where that realm's policy lets them make the call. `off` clears
|
|
885
|
+
// both caps; `on` keeps a cap it doesn't name, and removes one named `none`.
|
|
861
886
|
export async function keysAnonymous(
|
|
862
887
|
name: string,
|
|
863
888
|
setting: string,
|
|
864
|
-
options: {
|
|
889
|
+
options: {
|
|
890
|
+
cap?: string;
|
|
891
|
+
monthlyCap?: string;
|
|
892
|
+
json?: boolean;
|
|
893
|
+
} & KeysCommandDeps = {},
|
|
865
894
|
): Promise<UserKeyMetadata> {
|
|
866
895
|
if (setting !== 'on' && setting !== 'off') {
|
|
867
896
|
throw new Error('say on or off');
|
|
@@ -870,6 +899,12 @@ export async function keysAnonymous(
|
|
|
870
899
|
let anonymousDailyCap = anonymousCapOption(anonymous, options.cap, {
|
|
871
900
|
flag: '--cap',
|
|
872
901
|
with: 'on',
|
|
902
|
+
removable: true,
|
|
903
|
+
});
|
|
904
|
+
let anonymousMonthlyCap = anonymousCapOption(anonymous, options.monthlyCap, {
|
|
905
|
+
flag: '--monthly-cap',
|
|
906
|
+
with: 'on',
|
|
907
|
+
removable: true,
|
|
873
908
|
});
|
|
874
909
|
let ctx = context(options.json, options);
|
|
875
910
|
let token = await secureToken(ctx);
|
|
@@ -878,6 +913,7 @@ export async function keysAnonymous(
|
|
|
878
913
|
keyId: key.id,
|
|
879
914
|
anonymous,
|
|
880
915
|
...(anonymousDailyCap !== undefined ? { anonymousDailyCap } : {}),
|
|
916
|
+
...(anonymousMonthlyCap !== undefined ? { anonymousMonthlyCap } : {}),
|
|
881
917
|
});
|
|
882
918
|
emit(ctx, updated, () => {
|
|
883
919
|
console.log(
|
|
@@ -1017,6 +1053,206 @@ export async function keysShares(
|
|
|
1017
1053
|
return shares;
|
|
1018
1054
|
}
|
|
1019
1055
|
|
|
1056
|
+
export interface LimitsOptions {
|
|
1057
|
+
realm?: string;
|
|
1058
|
+
// `<burst>/<refill per minute>`; either side may be `default`.
|
|
1059
|
+
rate?: string;
|
|
1060
|
+
daily?: string;
|
|
1061
|
+
monthly?: string;
|
|
1062
|
+
json?: boolean;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// A limit as typed: a number, or `default` for the platform's default. The
|
|
1066
|
+
// realm server says which numbers a limit takes.
|
|
1067
|
+
function limitValue(option: string, value: string): number | null {
|
|
1068
|
+
if (value.trim().toLowerCase() === 'default') {
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1071
|
+
let number = Number(value);
|
|
1072
|
+
if (value.trim() === '' || !Number.isFinite(number)) {
|
|
1073
|
+
throw new Error(`${option} is a number, or default`);
|
|
1074
|
+
}
|
|
1075
|
+
return number;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function limitChanges(options: LimitsOptions): KeyLimitChanges {
|
|
1079
|
+
let changes: KeyLimitChanges = {};
|
|
1080
|
+
if (options.rate !== undefined) {
|
|
1081
|
+
// A bare `default` puts both halves back, as `--daily default` does its one.
|
|
1082
|
+
let parts =
|
|
1083
|
+
options.rate.trim().toLowerCase() === 'default'
|
|
1084
|
+
? ['default', 'default']
|
|
1085
|
+
: options.rate.split('/');
|
|
1086
|
+
if (parts.length !== 2) {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
'--rate is <burst>/<refill per minute>, such as 25/15; either may be default, or default for both',
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
changes.rateBurst = limitValue('--rate burst', parts[0]!);
|
|
1092
|
+
changes.rateRefillPerMinute = limitValue('--rate refill', parts[1]!);
|
|
1093
|
+
}
|
|
1094
|
+
if (options.daily !== undefined) {
|
|
1095
|
+
changes.dailyBudget = limitValue('--daily', options.daily);
|
|
1096
|
+
}
|
|
1097
|
+
if (options.monthly !== undefined) {
|
|
1098
|
+
changes.monthlyBudget = limitValue('--monthly', options.monthly);
|
|
1099
|
+
}
|
|
1100
|
+
return changes;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function printLimits(what: string, limits: KeyLimits): void {
|
|
1104
|
+
console.log(`Limits on ${what}:`);
|
|
1105
|
+
console.log(
|
|
1106
|
+
` rate: ${limits.rateBurst} calls at once per caller, refilled at ${limits.rateRefillPerMinute} a minute`,
|
|
1107
|
+
);
|
|
1108
|
+
console.log(` daily budget: ${limits.dailyBudget} calls`);
|
|
1109
|
+
console.log(` monthly budget: ${limits.monthlyBudget} calls`);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// A share named by its label with `--realm`, or else one of the user's own
|
|
1113
|
+
// keys by name.
|
|
1114
|
+
async function limitedTarget(
|
|
1115
|
+
ctx: Context,
|
|
1116
|
+
token: string,
|
|
1117
|
+
name: string,
|
|
1118
|
+
realm: string | undefined,
|
|
1119
|
+
): Promise<{
|
|
1120
|
+
what: string;
|
|
1121
|
+
target: { shareId: string } | { keyId: string };
|
|
1122
|
+
limits: KeyLimits | undefined;
|
|
1123
|
+
}> {
|
|
1124
|
+
if (realm) {
|
|
1125
|
+
let share = await findShare(ctx.realmServerUrl, token, {
|
|
1126
|
+
realm,
|
|
1127
|
+
label: name,
|
|
1128
|
+
});
|
|
1129
|
+
return {
|
|
1130
|
+
what: `${share.label} in ${share.realm}`,
|
|
1131
|
+
target: { shareId: share.id },
|
|
1132
|
+
limits: share.limits,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
let key = await findUserKey(ctx.realmServerUrl, token, name);
|
|
1136
|
+
return { what: key.name, target: { keyId: key.id }, limits: key.limits };
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Shows or sets the limits on how much of a key interactive calls may spend:
|
|
1140
|
+
// on a personal key, or with `--realm` on a share of one in that realm. Only
|
|
1141
|
+
// the key's owner sets them, with a session signed in on the secure origin;
|
|
1142
|
+
// a realm's other direct writers can read a share's but are refused changing
|
|
1143
|
+
// them, by the realm server. Without a change, prints the limits in force.
|
|
1144
|
+
export async function keysLimits(
|
|
1145
|
+
name: string,
|
|
1146
|
+
options: LimitsOptions & KeysCommandDeps,
|
|
1147
|
+
): Promise<KeyLimits> {
|
|
1148
|
+
let ctx = context(options.json, options);
|
|
1149
|
+
let changes = limitChanges(options);
|
|
1150
|
+
let changing = Object.keys(changes).length > 0;
|
|
1151
|
+
let token = changing ? await secureToken(ctx) : await ordinaryToken(ctx);
|
|
1152
|
+
let {
|
|
1153
|
+
what,
|
|
1154
|
+
target,
|
|
1155
|
+
limits: current,
|
|
1156
|
+
} = await limitedTarget(ctx, token, name, options.realm);
|
|
1157
|
+
let limits = changing
|
|
1158
|
+
? await setKeyLimits(ctx.realmServerUrl, token, target, changes)
|
|
1159
|
+
: current;
|
|
1160
|
+
if (!limits) {
|
|
1161
|
+
throw new Error(`The realm server didn't list the limits on ${what}`);
|
|
1162
|
+
}
|
|
1163
|
+
emit(ctx, limits, () => {
|
|
1164
|
+
if (changing) {
|
|
1165
|
+
console.log(`${FG_GREEN}Set the limits on ${what}.${RESET}`);
|
|
1166
|
+
}
|
|
1167
|
+
printLimits(what, limits);
|
|
1168
|
+
});
|
|
1169
|
+
return limits;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// The realm server names a caller only to someone who may see them in the
|
|
1173
|
+
// realm: one the reader can't see comes `withheld`, without a name, and a
|
|
1174
|
+
// call with no session comes without a name too.
|
|
1175
|
+
function callerName(
|
|
1176
|
+
ctx: Context,
|
|
1177
|
+
{ caller, withheld }: { caller: string | null; withheld?: boolean },
|
|
1178
|
+
): string {
|
|
1179
|
+
if (withheld) {
|
|
1180
|
+
return "someone you can't see in the realm";
|
|
1181
|
+
}
|
|
1182
|
+
return caller === null
|
|
1183
|
+
? 'anonymous'
|
|
1184
|
+
: caller === ctx.matrixUserId
|
|
1185
|
+
? 'you'
|
|
1186
|
+
: caller;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function printBudget(label: string, use: KeyUsage['budgets']['day']): void {
|
|
1190
|
+
console.log(
|
|
1191
|
+
` ${label}: ${use.used} of ${use.budget} calls ${DIM}(resets ${use.resetsAt})${RESET}`,
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function printUsage(ctx: Context, what: string, usage: KeyUsage): void {
|
|
1196
|
+
console.log(`Usage of ${what}:`);
|
|
1197
|
+
printBudget('today', usage.budgets.day);
|
|
1198
|
+
printBudget('this month', usage.budgets.month);
|
|
1199
|
+
console.log(` calls since ${usage.since}: ${usage.calls}`);
|
|
1200
|
+
if (usage.lastUse) {
|
|
1201
|
+
let { at, operation, outcome } = usage.lastUse;
|
|
1202
|
+
console.log(
|
|
1203
|
+
` last use: ${at}, ${operation} by ${callerName(ctx, usage.lastUse)} (${outcome})`,
|
|
1204
|
+
);
|
|
1205
|
+
} else {
|
|
1206
|
+
console.log(' last use: never');
|
|
1207
|
+
}
|
|
1208
|
+
if (usage.topCallers.length > 0) {
|
|
1209
|
+
console.log(' top callers:');
|
|
1210
|
+
for (let entry of usage.topCallers) {
|
|
1211
|
+
console.log(` ${callerName(ctx, entry)}: ${entry.calls}`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (usage.refusals.length > 0) {
|
|
1215
|
+
console.log(' refusals:');
|
|
1216
|
+
for (let { code, count } of usage.refusals) {
|
|
1217
|
+
console.log(` ${code}: ${count}`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (usage.vendorOutcomes.length > 0) {
|
|
1221
|
+
console.log(' vendor answers:');
|
|
1222
|
+
for (let { status, count, outcome } of usage.vendorOutcomes) {
|
|
1223
|
+
console.log(` ${status}${outcome ? ` (${outcome})` : ''}: ${count}`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
if (usage.grants.length > 0) {
|
|
1227
|
+
console.log(' grants:');
|
|
1228
|
+
for (let grant of usage.grants) {
|
|
1229
|
+
let state =
|
|
1230
|
+
grant.state === 'rejected'
|
|
1231
|
+
? `${FG_RED}rejected${RESET}${grant.rejectedAt ? ` ${grant.rejectedAt}` : ''}`
|
|
1232
|
+
: grant.state;
|
|
1233
|
+
console.log(` ${grant.destination}: ${state}`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
printLimits(what, usage.limits);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// What a share or a personal key has served: its budgets' use this UTC day
|
|
1240
|
+
// and month, its last use, top callers, refusals by reason, the vendor's
|
|
1241
|
+
// answers and its grants. A personal key's usage is for its owner; a share's,
|
|
1242
|
+
// named by its label with `--realm`, for the realm's direct writers. Reading
|
|
1243
|
+
// needs no secure session.
|
|
1244
|
+
export async function keysUsage(
|
|
1245
|
+
name: string,
|
|
1246
|
+
options: { realm?: string; json?: boolean } & KeysCommandDeps,
|
|
1247
|
+
): Promise<KeyUsage> {
|
|
1248
|
+
let ctx = context(options.json, options);
|
|
1249
|
+
let token = await ordinaryToken(ctx);
|
|
1250
|
+
let { what, target } = await limitedTarget(ctx, token, name, options.realm);
|
|
1251
|
+
let usage = await getKeyUsage(ctx.realmServerUrl, token, target);
|
|
1252
|
+
emit(ctx, usage, () => printUsage(ctx, what, usage));
|
|
1253
|
+
return usage;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1020
1256
|
// How `connect --client` names a client: `platform:<name>` for one of
|
|
1021
1257
|
// Boxel's own, a share's label for one shared into the realm given with
|
|
1022
1258
|
// `--realm`, and otherwise the name of one of the user's own client keys.
|
package/src/lib/key-bindings.ts
CHANGED
|
@@ -97,9 +97,10 @@ export interface BatchShare {
|
|
|
97
97
|
covers: string[];
|
|
98
98
|
interactive: boolean;
|
|
99
99
|
dailyBudget: number;
|
|
100
|
-
// Whether the share's key allows people who aren't signed in, and its
|
|
100
|
+
// Whether the share's key allows people who aren't signed in, and its caps.
|
|
101
101
|
anonymous?: boolean;
|
|
102
102
|
anonymousDailyCap?: number | null;
|
|
103
|
+
anonymousMonthlyCap?: number | null;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
export interface ApprovalBatch {
|