@cardstack/boxel-cli 0.12.0-unstable.13 → 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.
Files changed (56) hide show
  1. package/bundled-test-harness/assets/{ai-module-creation-test-D9FuBmi8.js → ai-module-creation-test-BuOdKEfn.js} +1 -1
  2. package/bundled-test-harness/assets/{app-B6ALqYQD.js → app-DFQ4JqlP.js} +2 -2
  3. package/bundled-test-harness/assets/{binary-upload-dedupe-test-eI1yGxdL.js → binary-upload-dedupe-test-jbrJQUz0.js} +1 -1
  4. package/bundled-test-harness/assets/{can-invoke-test-5enpsRYF.js → can-invoke-test-DoWzbZ8k.js} +1 -1
  5. package/bundled-test-harness/assets/{card-chooser-paging-test-CdtsXIpu.js → card-chooser-paging-test-VRmsX6sY.js} +1 -1
  6. package/bundled-test-harness/assets/{card-chooser-test-CpUK3_Su.js → card-chooser-test-CgrV_vWi.js} +1 -1
  7. package/bundled-test-harness/assets/{card-def-field-def-relationships-test-Z1O-t73y.js → card-def-field-def-relationships-test-BHQoFm4_.js} +1 -1
  8. package/bundled-test-harness/assets/{card-delete-test-DI16JAzu.js → card-delete-test-DCmoRd03.js} +1 -1
  9. package/bundled-test-harness/assets/{codeblocks-test-DEaQeMcn.js → codeblocks-test-DQU6dnQH.js} +1 -1
  10. package/bundled-test-harness/assets/{compaction-test-DliwzHTd.js → compaction-test-B7JuwjQ7.js} +1 -1
  11. package/bundled-test-harness/assets/{container-BHHKErdI.js → container-CwRfjGxi.js} +1 -1
  12. package/bundled-test-harness/assets/{create-listing-modal-test-BZiD6UL9.js → create-listing-modal-test-CxkuYwnv.js} +1 -1
  13. package/bundled-test-harness/assets/{debug-message-test-D7VEbny3.js → debug-message-test-CyrcsddQ.js} +1 -1
  14. package/bundled-test-harness/assets/{fallback-banner-live-delete-test-DwXohU1S.js → fallback-banner-live-delete-test-Bxxt9EBl.js} +1 -1
  15. package/bundled-test-harness/assets/{fallback-banner-test-jEmNTOeh.js → fallback-banner-test-BzmDOq0F.js} +1 -1
  16. package/bundled-test-harness/assets/{fallback-models-test-BfgkQJ8l.js → fallback-models-test-mZvUg3zI.js} +1 -1
  17. package/bundled-test-harness/assets/{file-attachment-test-CaX44AfG.js → file-attachment-test-ybs1HRMC.js} +1 -1
  18. package/bundled-test-harness/assets/{general-test-zz1QVTIv.js → general-test-jB4EQTh8.js} +1 -1
  19. package/bundled-test-harness/assets/{main-CYiHxOxo.js → main-uvgWNg1U.js} +1 -1
  20. package/bundled-test-harness/assets/{model-cost-tier-test-DcEeQJwF.js → model-cost-tier-test-BPCl0EeO.js} +1 -1
  21. package/bundled-test-harness/assets/{operator-mode-card-chooser-test-DYSf_9jJ.js → operator-mode-card-chooser-test-D8RfBsEc.js} +1 -1
  22. package/bundled-test-harness/assets/{operator-mode-links-test-BA3gjmzY.js → operator-mode-links-test-DmpvnpHI.js} +1 -1
  23. package/bundled-test-harness/assets/{operator-mode-test-C6iSqlfM.js → operator-mode-test-BeCWE8qH.js} +1 -1
  24. package/bundled-test-harness/assets/{operator-mode-ui-test-BFGw2suP.js → operator-mode-ui-test-D0k0pr1Z.js} +1 -1
  25. package/bundled-test-harness/assets/{overlay-menu-items-test-Ch351wNL.js → overlay-menu-items-test-l4-oOOi5.js} +1 -1
  26. package/bundled-test-harness/assets/{panel-B8BaMllL.js → panel-8nNZbvzg.js} +16 -4
  27. package/bundled-test-harness/assets/{past-sessions-test-DoVlENnJ.js → past-sessions-test-DrJ_h58w.js} +1 -1
  28. package/bundled-test-harness/assets/{permission-checking-realm-test-BrBeo81O.js → permission-checking-realm-test-CL_nySH1.js} +1 -1
  29. package/bundled-test-harness/assets/{quick-look-test-InFXyTqv.js → quick-look-test-DMfAGvgd.js} +1 -1
  30. package/bundled-test-harness/assets/{realm-api-keys-test-Bgg-gi56.js → realm-api-keys-test-DVkuepc8.js} +1 -1
  31. package/bundled-test-harness/assets/{realm-config-policy-standing-test-BIEwUQYA.js → realm-config-policy-standing-test-DXS8kYVC.js} +1 -1
  32. package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-CDj8rdvZ.js → realm-config-routing-rule-edit-test-CPTifvXx.js} +1 -1
  33. package/bundled-test-harness/assets/{realm-config-settings-test-Csi-H8bJ.js → realm-config-settings-test-Civ3ZVxT.js} +1 -1
  34. package/bundled-test-harness/assets/{reasoning-test-MNcXjMV3.js → reasoning-test-DYSQdtDZ.js} +1 -1
  35. package/bundled-test-harness/assets/{room-message-test-DxMyOu8L.js → room-message-test-CSbclniG.js} +1 -1
  36. package/bundled-test-harness/assets/{run-realm-code-test-DTXPkVu_.js → run-realm-code-test-B9KJIaYx.js} +81 -0
  37. package/bundled-test-harness/assets/{scrolling-test-6-Soq39G.js → scrolling-test-nDHEhgzp.js} +2 -2
  38. package/bundled-test-harness/assets/{sending-test-DXOw4MlE.js → sending-test-CjrOIeGV.js} +1 -1
  39. package/bundled-test-harness/assets/{skills-test-BiUE1x5l.js → skills-test-Be4crUyO.js} +1 -1
  40. package/bundled-test-harness/assets/{store-test-Cbf2xZ40.js → store-test-BCyLVXZY.js} +1 -1
  41. package/bundled-test-harness/assets/{tests-Blos5s0A.js → tests-DbU4zMsp.js} +43 -43
  42. package/bundled-test-harness/assets/{text-input-validator-test-BxG8Dc2i.js → text-input-validator-test-DZuVdEKj.js} +1 -1
  43. package/bundled-test-harness/assets/{to-device-streaming-test-CGuKaubC.js → to-device-streaming-test-C3i5WmnX.js} +1 -1
  44. package/bundled-test-harness/assets/{token-usage-test-DJJmXyD8.js → token-usage-test-1AWgVaXO.js} +1 -1
  45. package/bundled-test-harness/assets/{tools-test-DxDvBhPf.js → tools-test-BSmxEyfO.js} +1 -1
  46. package/bundled-test-harness/assets/{turn-settling-test-Co2Dmnzm.js → turn-settling-test-S3ROiZu9.js} +1 -1
  47. package/bundled-test-harness/index.html +5 -5
  48. package/bundled-test-harness/tests/index.html +5 -5
  49. package/bundled-types/host-app/services/tool-service.ts +36 -5
  50. package/dist/index.js +269 -269
  51. package/package.json +1 -1
  52. package/src/commands/keys/approve.ts +2 -1
  53. package/src/commands/keys/index.ts +12 -3
  54. package/src/commands/keys/manage.ts +43 -15
  55. package/src/lib/key-bindings.ts +2 -1
  56. package/src/lib/user-keys.ts +43 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardstack/boxel-cli",
3
- "version": "0.12.0-unstable.13",
3
+ "version": "0.12.0-unstable.14",
4
4
  "license": "MIT",
5
5
  "description": "CLI tools for Boxel workspace management",
6
6
  "main": "./dist/index.js",
@@ -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.anonymousDailyCap != null ? `, up to ${share.anonymousDailyCap} calls a day all together` : ''}.${RESET}`
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
  }
@@ -178,12 +178,17 @@ export function registerKeysCommand(program: Command): void {
178
178
  '--anonymous-cap <calls>',
179
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",
180
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
+ )
181
185
  .action(
182
186
  (
183
187
  name: string,
184
188
  options: ValueCommandOptions & {
185
189
  anonymous?: boolean;
186
190
  anonymousCap?: string;
191
+ anonymousMonthlyCap?: string;
187
192
  },
188
193
  ) => runKeysCommand(options.json, () => keysAdd(name, options)),
189
194
  );
@@ -191,20 +196,24 @@ export function registerKeysCommand(program: Command): void {
191
196
  keys
192
197
  .command('anonymous')
193
198
  .description(
194
- "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 the cap",
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",
195
200
  )
196
201
  .argument('<name>', 'The key')
197
202
  .argument('<on|off>', 'Allow it or not')
198
203
  .option(
199
204
  '--cap <calls>',
200
- "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",
201
210
  )
202
211
  .option('--json', 'Output JSON')
203
212
  .action(
204
213
  (
205
214
  name: string,
206
215
  setting: string,
207
- options: { cap?: string; json?: boolean },
216
+ options: { cap?: string; monthlyCap?: string; json?: boolean },
208
217
  ) =>
209
218
  runKeysCommand(options.json, () =>
210
219
  keysAnonymous(name, setting, options),
@@ -40,6 +40,7 @@ import {
40
40
  replaceUserKey,
41
41
  setKeyLimits,
42
42
  setUserKeyAnonymousUse,
43
+ anonymousCapsClause,
43
44
  shareUserKey,
44
45
  completeOAuthConnect,
45
46
  startOAuthConnect,
@@ -605,6 +606,7 @@ export async function keysAdd(
605
606
  json?: boolean;
606
607
  anonymous?: boolean;
607
608
  anonymousCap?: string;
609
+ anonymousMonthlyCap?: string;
608
610
  } & KeyValueSource &
609
611
  ServiceAccountOptions &
610
612
  OAuthClientOptions &
@@ -612,10 +614,13 @@ export async function keysAdd(
612
614
  ): Promise<UserKeyMetadata> {
613
615
  let ctx = context(options.json, options);
614
616
  let kind = options.kind ?? 'static';
615
- let anonymousDailyCap = anonymousCapOption(
616
- options.anonymous,
617
- options.anonymousCap,
618
- );
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;
619
624
  if (options.anonymous && kind === 'oauth-client') {
620
625
  throw new Error(
621
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`",
@@ -636,6 +641,7 @@ export async function keysAdd(
636
641
  ...oauthClientSettings(kind, options),
637
642
  ...(options.anonymous ? { anonymous: true } : {}),
638
643
  ...(anonymousDailyCap !== undefined ? { anonymousDailyCap } : {}),
644
+ ...(anonymousMonthlyCap !== undefined ? { anonymousMonthlyCap } : {}),
639
645
  });
640
646
  emit(ctx, key, () => {
641
647
  if (kind === 'oauth-client' && options.grantType !== 'client_credentials') {
@@ -826,31 +832,42 @@ async function warnWhereVisitorsWillUse(
826
832
  }
827
833
  if (!key.anonymous) {
828
834
  throw new Error(
829
- `${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)`,
830
836
  );
831
837
  }
832
838
  console.error(
833
- `${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.anonymousDailyCap != null ? `, up to ${key.anonymousDailyCap} calls a day all together` : ''}.${RESET}`,
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}`,
834
840
  );
835
841
  return true;
836
842
  }
837
843
 
838
- // `--anonymous-cap <calls>`, given only with `--anonymous` (or `on`): a whole
839
- // number of calls a day, from 1.
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.
840
848
  function anonymousCapOption(
841
849
  anonymous: boolean | undefined,
842
850
  cap: string | undefined,
843
- { flag = '--anonymous-cap', with: withFlag = '--anonymous' } = {},
844
- ): number | undefined {
851
+ {
852
+ flag = '--anonymous-cap',
853
+ with: withFlag = '--anonymous',
854
+ removable = false,
855
+ } = {},
856
+ ): number | null | undefined {
845
857
  if (cap === undefined) {
846
858
  return undefined;
847
859
  }
848
860
  if (!anonymous) {
849
861
  throw new Error(`${flag} is given with ${withFlag}`);
850
862
  }
863
+ if (removable && cap.trim().toLowerCase() === 'none') {
864
+ return null;
865
+ }
851
866
  let calls = Number(cap);
852
867
  if (!/^\d+$/.test(cap.trim()) || calls < 1 || calls > 2_147_483_647) {
853
- throw new Error(`${flag} is a whole number of calls, from 1 to 2147483647`);
868
+ throw new Error(
869
+ `${flag} is a whole number of calls, from 1 to 2147483647${removable ? ', or none' : ''}`,
870
+ );
854
871
  }
855
872
  return calls;
856
873
  }
@@ -858,18 +875,22 @@ function anonymousCapOption(
858
875
  // How a key's setting for people who aren't signed in reads.
859
876
  function anonymousUseOf(key: UserKeyMetadata): string {
860
877
  return key.anonymous
861
- ? `allowed${key.anonymousDailyCap != null ? `, up to ${key.anonymousDailyCap} calls a day all together` : ''}, wherever a realm's policy lets them make the call`
878
+ ? `allowed${anonymousCapsClause(key)}, wherever a realm's policy lets them make the call`
862
879
  : 'not allowed';
863
880
  }
864
881
 
865
882
  // Whether people who aren't signed in may use one of the user's keys: its
866
883
  // owner's setting for the key itself, which every realm it is shared into
867
- // follows where that realm's policy lets them make the call. `off` clears the
868
- // 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`.
869
886
  export async function keysAnonymous(
870
887
  name: string,
871
888
  setting: string,
872
- options: { cap?: string; json?: boolean } & KeysCommandDeps = {},
889
+ options: {
890
+ cap?: string;
891
+ monthlyCap?: string;
892
+ json?: boolean;
893
+ } & KeysCommandDeps = {},
873
894
  ): Promise<UserKeyMetadata> {
874
895
  if (setting !== 'on' && setting !== 'off') {
875
896
  throw new Error('say on or off');
@@ -878,6 +899,12 @@ export async function keysAnonymous(
878
899
  let anonymousDailyCap = anonymousCapOption(anonymous, options.cap, {
879
900
  flag: '--cap',
880
901
  with: 'on',
902
+ removable: true,
903
+ });
904
+ let anonymousMonthlyCap = anonymousCapOption(anonymous, options.monthlyCap, {
905
+ flag: '--monthly-cap',
906
+ with: 'on',
907
+ removable: true,
881
908
  });
882
909
  let ctx = context(options.json, options);
883
910
  let token = await secureToken(ctx);
@@ -886,6 +913,7 @@ export async function keysAnonymous(
886
913
  keyId: key.id,
887
914
  anonymous,
888
915
  ...(anonymousDailyCap !== undefined ? { anonymousDailyCap } : {}),
916
+ ...(anonymousMonthlyCap !== undefined ? { anonymousMonthlyCap } : {}),
889
917
  });
890
918
  emit(ctx, updated, () => {
891
919
  console.log(
@@ -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 cap.
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 {
@@ -100,10 +100,35 @@ export interface UserKeyMetadata {
100
100
  createdAt: string;
101
101
  updatedAt: string;
102
102
  // Whether its owner lets people who aren't signed in use it, wherever a
103
- // realm's policy lets them make the call, and the optional cap on their
104
- // calls a day, all together, in every realm it is shared into.
103
+ // realm's policy lets them make the call, and the optional caps on their
104
+ // calls a day and a month, all together, in every realm it is shared into.
105
105
  anonymous?: boolean;
106
106
  anonymousDailyCap?: number | null;
107
+ anonymousMonthlyCap?: number | null;
108
+ }
109
+
110
+ // A key's anonymous caps as a clause that ends a sentence about people who
111
+ // aren't signed in: ", up to 100 calls a day and 1000 a month all together",
112
+ // or nothing where the key has neither.
113
+ export function anonymousCapsClause({
114
+ anonymousDailyCap,
115
+ anonymousMonthlyCap,
116
+ }: {
117
+ anonymousDailyCap?: number | null;
118
+ anonymousMonthlyCap?: number | null;
119
+ }): string {
120
+ let daily = anonymousDailyCap ?? null;
121
+ let monthly = anonymousMonthlyCap ?? null;
122
+ if (daily === null && monthly === null) {
123
+ return '';
124
+ }
125
+ let caps =
126
+ daily !== null && monthly !== null
127
+ ? `${daily} calls a day and ${monthly} a month`
128
+ : daily !== null
129
+ ? `${daily} calls a day`
130
+ : `${monthly} calls a month`;
131
+ return `, up to ${caps} all together`;
107
132
  }
108
133
 
109
134
  export interface KeyShare {
@@ -311,6 +336,7 @@ export async function addUserKey(
311
336
  ticket,
312
337
  anonymous,
313
338
  anonymousDailyCap,
339
+ anonymousMonthlyCap,
314
340
  ...entered
315
341
  }: {
316
342
  name: string;
@@ -318,6 +344,7 @@ export async function addUserKey(
318
344
  ticket?: string;
319
345
  anonymous?: boolean;
320
346
  anonymousDailyCap?: number;
347
+ anonymousMonthlyCap?: number;
321
348
  } & EnteredValue,
322
349
  fetchFn: FetchFn = fetch,
323
350
  ): Promise<UserKeyMetadata> {
@@ -340,6 +367,7 @@ export async function addUserKey(
340
367
  ...valueAttributes(entered),
341
368
  ...(anonymous ? { anonymous: true } : {}),
342
369
  ...(anonymousDailyCap !== undefined ? { anonymousDailyCap } : {}),
370
+ ...(anonymousMonthlyCap !== undefined ? { anonymousMonthlyCap } : {}),
343
371
  },
344
372
  },
345
373
  meta: { ticket },
@@ -376,7 +404,8 @@ export async function realmOpensToAnonymous(
376
404
  }
377
405
 
378
406
  // Sets whether people who aren't signed in may use one of the user's keys,
379
- // and the optional cap on their calls a day. Takes a secure session.
407
+ // and the optional caps on their calls a day and a month. A cap left out is
408
+ // kept, and null removes it. Takes a secure session.
380
409
  export async function setUserKeyAnonymousUse(
381
410
  realmServerUrl: string,
382
411
  token: string,
@@ -384,7 +413,14 @@ export async function setUserKeyAnonymousUse(
384
413
  keyId,
385
414
  anonymous,
386
415
  anonymousDailyCap,
387
- }: { keyId: string; anonymous: boolean; anonymousDailyCap?: number },
416
+ anonymousMonthlyCap,
417
+ }: {
418
+ keyId: string;
419
+ anonymous: boolean;
420
+ // A cap left out is kept, and null removes it.
421
+ anonymousDailyCap?: number | null;
422
+ anonymousMonthlyCap?: number | null;
423
+ },
388
424
  fetchFn: FetchFn = fetch,
389
425
  ): Promise<UserKeyMetadata> {
390
426
  let json = await realmServerRequest(
@@ -400,6 +436,9 @@ export async function setUserKeyAnonymousUse(
400
436
  attributes: {
401
437
  anonymous,
402
438
  ...(anonymousDailyCap !== undefined ? { anonymousDailyCap } : {}),
439
+ ...(anonymousMonthlyCap !== undefined
440
+ ? { anonymousMonthlyCap }
441
+ : {}),
403
442
  },
404
443
  },
405
444
  },