@bivy/bivy 0.18.11-staging.2 → 0.18.11-staging.3

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/bin/bivy.mjs CHANGED
@@ -2472,7 +2472,7 @@ async function cmdApp(args = []) {
2472
2472
  bivy app remove <app-id> [--session <id>]
2473
2473
  bivy app shot [app-id] [--widths 390,1280] [--themes light,dark] [--path /page] [--session <id>]
2474
2474
  bivy app present [view] [--path /page] [--note "…"] [--session <id>]
2475
- bivy app share [app-id] [--view <view>] [--session <id>]
2475
+ bivy app share [app-id] [--view <view>] [--for 1h|1d|7d] [--view-only] [--session <id>]
2476
2476
  bivy app notes [app-id] [--view <view>] [--since <time>] [--session <id>]
2477
2477
  bivy app run [--name <name>] [--restart-on-change] [--session <id>] -- <command> [args…]
2478
2478
  bivy app click <x> <y> [--right|--middle] [--double] [--app <app>]
@@ -2518,9 +2518,10 @@ the chat with the app at phone width (and before/after), and it opens the live
2518
2518
  preview. Use it when you finish a visible change, or when the user asks to see
2519
2519
  it. [view] is an app or view name or ID (default: the one opened last).
2520
2520
  "share" mints a reusable link to one web view and prints its URL and expiry
2521
- (24 hours, or until Revoke access in Apps, the app is removed or the machine
2522
- restarts). [app-id] is an app ID or name, --view a view ID or name (default: the
2523
- one opened last). Anyone who has the link can use the app, including a live
2521
+ (--for: 1h, 1d or 7d, default 1d; sooner if the user stops sharing in Apps, the
2522
+ app is removed or the machine restarts). [app-id] is an app ID or name, --view
2523
+ a view ID or name (default: the one opened last). --view-only leaves out the
2524
+ reviewer tools (Point, Draw, notes): people see just the app. Anyone who has the link can use the app, including a live
2524
2525
  server's backend, until it expires or is revoked: only post it where everyone
2525
2526
  who can read it may use the app.
2526
2527
  "notes" prints the notes people left through share links (Leave a note), as
@@ -2557,6 +2558,17 @@ Web previews require operator setup; see docs/apps.md.`);
2557
2558
  rest.splice(i, 2);
2558
2559
  }
2559
2560
  }
2561
+ if (action === "share") {
2562
+ const i = rest.indexOf("--for");
2563
+ if (i >= 0) {
2564
+ const value = rest[i + 1];
2565
+ if (!["1h", "1d", "7d"].includes(value)) throw new Error("--for takes 1h, 1d or 7d.");
2566
+ shot.duration = value;
2567
+ rest.splice(i, 2);
2568
+ }
2569
+ const viewOnly = rest.indexOf("--view-only");
2570
+ if (viewOnly >= 0) { shot.controls = false; rest.splice(viewOnly, 1); }
2571
+ }
2560
2572
  if (action === "notes") {
2561
2573
  const i = rest.indexOf("--since");
2562
2574
  if (i >= 0) {
@@ -2656,7 +2668,7 @@ async function appRequest(action, body, { print = true } = {}) {
2656
2668
  if (action === "share") {
2657
2669
  const { type: _type, requestId: _requestId, ...link } = result;
2658
2670
  console.log(JSON.stringify({ ...link, expires: new Date(link.expiresAt).toISOString() }, null, 2));
2659
- console.error("Anyone with this link can use the app, including its live backend, until it expires or you revoke it (Apps → Revoke access).");
2671
+ console.error("Anyone with this link can use the app, including its live backend, until it expires or the user stops sharing (Apps → Share → Stop sharing).");
2660
2672
  return;
2661
2673
  }
2662
2674
  if (print) console.log(JSON.stringify(result, null, 2));
@@ -132,6 +132,8 @@ const poll=async()=>{try{const r=await fetch(location.href,{method:'HEAD',cache:
132
132
  }
133
133
  /** Copied links are reusable until revoked, capped so a forgotten one lapses. */
134
134
  export const SHARE_TTL = 24 * HOUR;
135
+ /** The longest a copied link may be asked to last. */
136
+ export const SHARE_TTL_MAX = 7 * 24 * HOUR;
135
137
  /** Bivy's packaged apps load the client from their own scheme, so a preview
136
138
  * framed there has this ancestor instead of a web origin. (Node's URL gives
137
139
  * these "null" origins, so they cannot be configured as return origins.) */
@@ -204,9 +206,13 @@ export class AppGateway {
204
206
  ["/__bivy/styles.css", readFileSync(new URL("./styles.css", import.meta.url))],
205
207
  ]);
206
208
  /** `reviewer`: the browser session came from a copied (shared) link.
209
+ * `viewOnly`: that link came without the reviewer tools.
207
210
  * `embedded`: it runs framed inside a Bivy client (a third-party frame). */
208
211
  sessions = new Map();
209
212
  sockets = new Map();
213
+ /** Connections opened by people with a copied link, so Stop sharing can end
214
+ * theirs and leave the owner's. */
215
+ shared = new WeakSet();
210
216
  captureAfter = 0;
211
217
  capturing = false;
212
218
  displays = new WebSocketServer({ noServer: true, maxPayload: 1024 * 1024, perMessageDeflate: false });
@@ -255,11 +261,33 @@ export class AppGateway {
255
261
  return `${this.origin(id)}${OPEN_PATH}#${this.grant({ appId: id, expires: Date.now() + 60_000 })}`;
256
262
  }
257
263
  /** Public reviewers use the same isolated controls as the owner, but can
258
- * only submit notes. Valid until revoked or SHARE_TTL. */
259
- share(id) {
264
+ * only submit notes; without `controls` they only see the app. Valid until
265
+ * revoked, sharing stops, or `ttl` (SHARE_TTL by default). */
266
+ share(id, { ttl = SHARE_TTL, controls = true } = {}) {
260
267
  this.requireWeb(id);
261
- const expiresAt = Date.now() + SHARE_TTL;
262
- return { url: `${this.shellOrigin(id)}${OPEN_PATH}#${this.grant({ appId: id, expires: expiresAt, reusable: true })}`, expiresAt };
268
+ if (!Number.isFinite(ttl) || ttl <= 0 || ttl > SHARE_TTL_MAX)
269
+ throw new Error("A share link can last at most 7 days.");
270
+ const expiresAt = Date.now() + ttl;
271
+ return { url: `${this.shellOrigin(id)}${OPEN_PATH}#${this.grant({ appId: id, expires: expiresAt, reusable: true, ...(controls ? {} : { viewOnly: true }) })}`, expiresAt, controls };
272
+ }
273
+ /** Copied links that still work: how many, and when the last one lapses. */
274
+ sharing(id) {
275
+ this.sweep();
276
+ const links = [...this.tickets.values()].filter((grant) => grant.appId === id && grant.reusable);
277
+ return links.length ? { links: links.length, expiresAt: Math.max(...links.map((grant) => grant.expires)) } : undefined;
278
+ }
279
+ /** Ends every copied link and the browser sessions and connections opened
280
+ * from them. The owner's own previews keep working. */
281
+ unshare(id) {
282
+ for (const [key, grant] of this.tickets)
283
+ if (grant.appId === id && grant.reusable)
284
+ this.tickets.delete(key);
285
+ for (const [key, session] of this.sessions)
286
+ if (session.appId === id && session.reviewer)
287
+ this.sessions.delete(key);
288
+ for (const socket of this.sockets.get(id) ?? [])
289
+ if (this.shared.has(socket))
290
+ socket.destroy();
263
291
  }
264
292
  revoke(id) {
265
293
  for (const map of [this.tickets, this.sessions])
@@ -313,13 +341,15 @@ export class AppGateway {
313
341
  const session = token ? this.sessions.get(token) : undefined;
314
342
  return session?.appId === id ? session : undefined;
315
343
  }
316
- authorize(req, id) { return this.session(req, id)?.expires; }
317
- track(id, socket, expires) {
344
+ track(id, socket, session) {
345
+ const expires = session.expires;
318
346
  let sockets = this.sockets.get(id);
319
347
  if (!sockets) {
320
348
  sockets = new Set();
321
349
  this.sockets.set(id, sockets);
322
350
  }
351
+ if (session.reviewer)
352
+ this.shared.add(socket);
323
353
  if (sockets.has(socket))
324
354
  return;
325
355
  sockets.add(socket);
@@ -369,7 +399,7 @@ export class AppGateway {
369
399
  res.setHeader("Content-Type", "application/json");
370
400
  // Someone opening a shared link sees "Made with Bivy" under the app; the owner doesn't.
371
401
  const badge = grant.reusable && this.badge ? await this.badge().catch(() => true) : false;
372
- res.end(JSON.stringify({ name: entry.app.name, origin: this.origin(id), returnTo: grant.returnTo, ...(grant.reusable ? { reviewer: true } : {}), ...(badge ? { badge: true } : {}), ...(entry.target.kind === "display" ? { inspect: false } : {}) }));
402
+ res.end(JSON.stringify({ name: entry.app.name, origin: this.origin(id), returnTo: grant.returnTo, ...(grant.reusable ? { reviewer: true } : {}), ...(grant.viewOnly ? { controls: false } : {}), ...(badge ? { badge: true } : {}), ...(entry.target.kind === "display" ? { inspect: false } : {}) }));
373
403
  return;
374
404
  }
375
405
  res.writeHead(404);
@@ -433,7 +463,7 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
433
463
  // A browser session never outlives the link that created it.
434
464
  const expires = Math.min(Date.now() + HOUR, grant.expires);
435
465
  const token = randomBytes(32).toString("hex");
436
- this.sessions.set(token, { appId: id, expires, reviewer: grant.reusable === true, ...(embedded ? { embedded } : {}) });
466
+ this.sessions.set(token, { appId: id, expires, reviewer: grant.reusable === true, ...(grant.viewOnly ? { viewOnly: true } : {}), ...(embedded ? { embedded } : {}) });
437
467
  // Framed inside Bivy, the cookie is third-party: it must be SameSite=None,
438
468
  // and Partitioned keys it to Bivy's top-level site so no other site can use it.
439
469
  const scope = embedded ? "SameSite=None; Partitioned" : "SameSite=Lax";
@@ -442,16 +472,16 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
442
472
  res.end();
443
473
  return;
444
474
  }
445
- const session = this.session(req, id), expires = session?.expires;
475
+ const session = this.session(req, id);
446
476
  // A home-screen visit without access goes through Bivy to sign in, then
447
477
  // comes back to the same page. Framed loads keep the plain message.
448
- const signIn = !expires && req.method === "GET" && req.headers["sec-fetch-dest"] === "document" && req.url?.startsWith("/") ? this.signIn?.(entry, req.url) : undefined;
478
+ const signIn = !session && req.method === "GET" && req.headers["sec-fetch-dest"] === "document" && req.url?.startsWith("/") ? this.signIn?.(entry, req.url) : undefined;
449
479
  if (signIn) {
450
480
  res.writeHead(303, { location: signIn });
451
481
  res.end();
452
482
  return;
453
483
  }
454
- if (!expires) {
484
+ if (!session) {
455
485
  res.writeHead(401);
456
486
  res.end("Preview access expired. Open a new preview link from Bivy.");
457
487
  return;
@@ -472,7 +502,7 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
472
502
  res.end();
473
503
  return;
474
504
  }
475
- this.track(id, req.socket, expires);
505
+ this.track(id, req.socket, session);
476
506
  if (req.url.startsWith(`${REVISION_PATH}?`) && req.method === "GET") {
477
507
  this.revision(req, res, entry);
478
508
  return;
@@ -483,10 +513,16 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
483
513
  }
484
514
  if (req.url === INSPECTOR_PATH && req.method === "GET") {
485
515
  res.writeHead(200, { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store" });
486
- res.end(inspectorScript(this.shellOrigin(id), session?.reviewer === true, session?.embedded === true));
516
+ res.end(inspectorScript(this.shellOrigin(id), session.reviewer === true && !session.viewOnly, session.embedded === true));
487
517
  return;
488
518
  }
489
519
  if (req.url === NOTES_PATH && req.method === "POST") {
520
+ // A view-only link shows the app and nothing else.
521
+ if (session.viewOnly) {
522
+ res.writeHead(403);
523
+ res.end();
524
+ return;
525
+ }
490
526
  await this.note(req, res, entry);
491
527
  return;
492
528
  }
@@ -880,13 +916,13 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
880
916
  }
881
917
  upgrade(req, socket, head) {
882
918
  const entry = this.entry(req);
883
- const expires = entry && this.authorize(req, entry.view.id);
919
+ const session = entry && this.session(req, entry.view.id);
884
920
  const display = entry?.target.kind === "display";
885
- if (!entry || !expires || (entry.target.kind !== "service" && !(display && req.url === DISPLAY_SOCKET_PATH)) || req.headers.origin !== this.origin(entry.view.id) || req.headers.upgrade?.toLowerCase() !== "websocket" || !req.url?.startsWith("/") || req.url.startsWith("//")) {
921
+ if (!entry || !session || (entry.target.kind !== "service" && !(display && req.url === DISPLAY_SOCKET_PATH)) || req.headers.origin !== this.origin(entry.view.id) || req.headers.upgrade?.toLowerCase() !== "websocket" || !req.url?.startsWith("/") || req.url.startsWith("//")) {
886
922
  socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
887
923
  return;
888
924
  }
889
- this.track(entry.view.id, socket, expires);
925
+ this.track(entry.view.id, socket, session);
890
926
  if (display) {
891
927
  this.displaySocket(req, socket, head, entry);
892
928
  return;
@@ -898,7 +934,7 @@ fetch('${REDEEM_PATH}',{method:'POST',headers:{'Content-Type':'text/plain'},body
898
934
  upstream.setTimeout(10_000, () => upstream.destroy());
899
935
  upstream.on("upgrade", (response, peer, upstreamHead) => {
900
936
  peer.setTimeout(0);
901
- this.track(entry.view.id, peer, expires);
937
+ this.track(entry.view.id, peer, session);
902
938
  const headers = cleanHeaders(response.headers);
903
939
  delete headers["set-cookie"];
904
940
  socket.write(`HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n${Object.entries(headers).filter(([, value]) => value !== undefined).map(([key, value]) => `${key}: ${value}\r\n`).join("")}\r\n`);
@@ -162,6 +162,8 @@ function show(data,launch){
162
162
  $('draft-text').maxLength=1000;
163
163
  $('send-errors').textContent='Leave a note…';
164
164
  }
165
+ // A view-only share link shows the app without Bivy's tools.
166
+ $('dock').hidden=data.controls===false;
165
167
  $('made-with').hidden=!data.badge;
166
168
  document.body.classList.toggle('badged',Boolean(data.badge));
167
169
  $('title').textContent=data.name;
@@ -65,11 +65,13 @@ export class RemotePreview {
65
65
  throw new Error("Preview delivery is unavailable. Reconnect the machine and try again.");
66
66
  return this.gateway.open(id, returnTo);
67
67
  }
68
- share(id) {
68
+ share(id, options) {
69
69
  if (!this.available)
70
70
  throw new Error("Preview delivery is unavailable. Reconnect the machine and try again.");
71
- return this.gateway.share(id);
71
+ return this.gateway.share(id, options);
72
72
  }
73
+ sharing(id) { return this.gateway?.sharing(id); }
74
+ unshare(id) { this.gateway?.unshare(id); }
73
75
  revoke(id) { this.gateway?.revoke(id); }
74
76
  address(id) { return this.available ? this.gateway.address(id) : undefined; }
75
77
  openDirect(id) {
@@ -1,3 +1,6 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ import { SHARE_DURATIONS } from "./types.js";
1
4
  import { randomBytes } from "node:crypto";
2
5
  import { REVIEW_MODES, shouldReview, visualChange } from "./review.js";
3
6
  import fs from "node:fs";
@@ -73,6 +76,9 @@ export class AppService {
73
76
  view.stats = structuredClone(entry.stats);
74
77
  if (entry?.lastPath)
75
78
  view.lastPath = entry.lastPath;
79
+ const sharing = this.gateway?.sharing?.(view.id);
80
+ if (sharing)
81
+ view.sharing = sharing;
76
82
  }
77
83
  return { apps, previewAvailable: available };
78
84
  }
@@ -577,18 +583,21 @@ export class AppService {
577
583
  void server.pending?.then((termId) => this.terminals.close(termId)).catch(() => { });
578
584
  this.servers.delete(id);
579
585
  }
580
- share(sessionId, appId, viewId) {
586
+ share(sessionId, appId, viewId, options = {}) {
581
587
  if (this.registry.requireView(sessionId, appId, viewId).view.kind !== "web")
582
588
  throw new Error("Only web views have preview links.");
583
589
  if (!this.gateway)
584
590
  throw new Error("Bivy's preview service is unavailable on this connection.");
585
- return this.gateway.share(viewId);
591
+ const duration = options.duration === undefined ? undefined : SHARE_DURATIONS.find((row) => row.id === options.duration);
592
+ if (options.duration !== undefined && !duration)
593
+ throw new Error(`A share link lasts ${SHARE_DURATIONS.map((row) => row.id).join(", ")}.`);
594
+ return this.gateway.share(viewId, { ...(duration ? { ttl: duration.ms } : {}), controls: options.controls !== false });
586
595
  }
587
596
  /** `bivy app share`: mints a share link for a web view picked by app and/or
588
597
  * view ID or name, like `present` (default: the one opened last). */
589
598
  shareView(sessionId, input) {
590
599
  const entry = this.pickTarget(sessionId, input);
591
- return { ...this.share(sessionId, entry.app.id, entry.view.id), appId: entry.app.id, viewId: entry.view.id, app: entry.app.name, view: entry.view.name };
600
+ return { ...this.share(sessionId, entry.app.id, entry.view.id, input), appId: entry.app.id, viewId: entry.view.id, app: entry.app.name, view: entry.view.name };
592
601
  }
593
602
  clearNotes(sessionId, appId, viewId) {
594
603
  this.registry.requireView(sessionId, appId, viewId);
@@ -623,6 +632,12 @@ export class AppService {
623
632
  throw new Error(`No web view called "${input.view}"${input.app ? ` in "${input.app}"` : ""}. Run bivy app list to see them.`);
624
633
  return found;
625
634
  }
635
+ /** Ends a view's share links and what people opened with them; the owner's previews and the app stay. */
636
+ unshare(sessionId, appId, viewId) {
637
+ this.registry.requireView(sessionId, appId, viewId);
638
+ this.gateway?.unshare?.(viewId);
639
+ return { ok: true };
640
+ }
626
641
  /** Ends every link, browser session and open connection for one view; the app stays. */
627
642
  revoke(sessionId, appId, viewId) {
628
643
  this.registry.requireView(sessionId, appId, viewId);
@@ -3147,6 +3147,9 @@ p.setup-note .btn.link { min-height: 44px; margin-block: calc((1lh - 44px) / 2);
3147
3147
  .apps-empty strong { color: var(--ink); font-size: var(--text-base); }
3148
3148
  .apps-empty p { margin: 0; max-inline-size: 34ch; }
3149
3149
  .apps-foot { margin: 0; padding: 0 var(--space-1); color: var(--muted); font-size: var(--text-xs); line-height: var(--lh-xs); }
3150
+ /* Share sheet (AppAccess.tsx): the sharing status sits in the sheet, not at page edge. */
3151
+ .apps-sheet > .banner { border-radius: var(--radius-md); padding: var(--space-2) var(--space-3); }
3152
+ .apps-sheet > .settings-toggle-row { padding-inline: var(--space-1); }
3150
3153
  /* Peek: an app preview framed in a drawer over the chat (PreviewPeek.tsx). */
3151
3154
  .preview-peek { display: block; inline-size: 100%; block-size: 100%; border: 0; border-radius: var(--radius-md); background: var(--bg); }
3152
3155
  /* Share-sheet landing (ShareDestinationSheet.tsx): what was shared, then one
@@ -24,3 +24,11 @@ export function isAppReference(value) {
24
24
  const ref = value;
25
25
  return typeof ref.appId === "string" && /^[a-f0-9]{32}$/.test(ref.appId) && typeof ref.sessionId === "string" && ref.sessionId.length > 0 && typeof ref.name === "string" && ref.name.length <= 100;
26
26
  }
27
+ /** How long a new share link works. Links live in the machine's memory, so a
28
+ * restart ends them sooner. Adding a choice means adding a row. */
29
+ export const SHARE_DURATIONS = [
30
+ { id: "1h", label: "1 hour", ms: 3_600_000 },
31
+ { id: "1d", label: "1 day", ms: 86_400_000 },
32
+ { id: "7d", label: "7 days", ms: 7 * 86_400_000 },
33
+ ];
34
+ export const DEFAULT_SHARE_DURATION = "1d";
@@ -34,9 +34,13 @@ export function createAppCommands(service, workspaceFor, published = () => { })
34
34
  "apps.notes": (msg) => service.notes(String(msg.sessionId), { app: typeof msg.appId === "string" ? msg.appId : undefined, view: typeof msg.view === "string" ? msg.view : undefined, since: typeof msg.since === "number" ? msg.since : undefined }),
35
35
  "apps.logs": (msg) => service.logs(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
36
36
  // The app UI passes exact IDs; `bivy app share` passes an app and/or view by ID or name.
37
- "apps.share": (msg) => typeof msg.viewId === "string" && typeof msg.appId === "string"
38
- ? service.share(String(msg.sessionId), msg.appId, msg.viewId)
39
- : service.shareView(String(msg.sessionId), { app: typeof msg.appId === "string" ? msg.appId : undefined, view: typeof msg.view === "string" ? msg.view : undefined }),
37
+ "apps.share": (msg) => {
38
+ const options = { duration: msg.duration, controls: msg.controls !== false };
39
+ return typeof msg.viewId === "string" && typeof msg.appId === "string"
40
+ ? service.share(String(msg.sessionId), msg.appId, msg.viewId, options)
41
+ : service.shareView(String(msg.sessionId), { app: typeof msg.appId === "string" ? msg.appId : undefined, view: typeof msg.view === "string" ? msg.view : undefined, ...options });
42
+ },
43
+ "apps.unshare": (msg) => service.unshare(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
40
44
  "apps.revoke": (msg) => service.revoke(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
41
45
  "apps.remove": (msg) => { service.remove(String(msg.sessionId), String(msg.appId)); return { ok: true }; },
42
46
  };
@@ -3,7 +3,7 @@
3
3
  /** Direct-mode adapters for operations whose canonical implementation is the
4
4
  * client command registry. Relay and HTTP now differ only in framing. */
5
5
  export const CLIENT_COMMAND_ROUTES = [
6
- ...["list", "publish", "offers", "adopt", "open", "logs", "shot", "input", "menu", "present", "showMe", "mute", "reviewMode", "annotate", "clearNotes", "agentNotes", "notes", "share", "revoke", "remove"].map((action) => ({ method: "post", path: `/api/apps/${action}`, kind: `apps.${action}` })),
6
+ ...["list", "publish", "offers", "adopt", "open", "logs", "shot", "input", "menu", "present", "showMe", "mute", "reviewMode", "annotate", "clearNotes", "agentNotes", "notes", "share", "unshare", "revoke", "remove"].map((action) => ({ method: "post", path: `/api/apps/${action}`, kind: `apps.${action}` })),
7
7
  { method: "post", path: "/api/artifacts/list", kind: "artifacts.list" },
8
8
  { method: "post", path: "/api/auth/credentials/native-preview", kind: "credentials.native.preview" },
9
9
  { method: "post", path: "/api/auth/credentials/native-import", kind: "credentials.native.import" },
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright (c) 2026 Petter André Sjulstad
3
3
  import { Type } from "typebox";
4
+ import { SHARE_DURATIONS } from "../apps/types.js";
4
5
  const request = { requestId: Type.Optional(Type.String()) };
5
6
  const session = { sessionId: Type.String() };
6
7
  const optionalSession = { sessionId: Type.Optional(Type.String()) };
@@ -31,7 +32,9 @@ export const CLIENT_COMMAND_SCHEMAS = {
31
32
  "apps.agentNotes": Type.Object({ ...request, ...session, appId: Type.String(), enabled: Type.Boolean() }),
32
33
  "apps.notes": Type.Object({ ...request, ...session, appId: Type.Optional(Type.String({ maxLength: 200 })), view: Type.Optional(Type.String({ maxLength: 200 })), since: Type.Optional(Type.Number()) }),
33
34
  "apps.logs": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String() }),
34
- "apps.share": Type.Object({ ...request, ...session, appId: Type.Optional(Type.String({ maxLength: 200 })), viewId: Type.Optional(Type.String()), view: Type.Optional(Type.String({ maxLength: 200 })) }),
35
+ "apps.share": Type.Object({ ...request, ...session, appId: Type.Optional(Type.String({ maxLength: 200 })), viewId: Type.Optional(Type.String()), view: Type.Optional(Type.String({ maxLength: 200 })),
36
+ duration: Type.Optional(Type.Union(SHARE_DURATIONS.map((row) => Type.Literal(row.id)))), controls: Type.Optional(Type.Boolean()) }),
37
+ "apps.unshare": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String() }),
35
38
  "apps.revoke": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String() }),
36
39
  "apps.remove": Type.Object({ ...request, ...session, appId: Type.String() }),
37
40
  "credentials.native.preview": Type.Object({ ...request, label: Type.Optional(Type.String({ maxLength: 100 })) }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.18.11-staging.2",
3
+ "version": "0.18.11-staging.3",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",