@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 +17 -5
- package/dist/apps/gateway.js +53 -17
- package/dist/apps/preview-shell.js +2 -0
- package/dist/apps/remote-preview.js +4 -2
- package/dist/apps/service.js +18 -3
- package/dist/apps/styles.css +3 -0
- package/dist/apps/types.js +8 -0
- package/dist/controllers/app-commands.js +7 -3
- package/dist/protocol/client-command-routes.js +1 -1
- package/dist/protocol/client-command-schemas.js +4 -1
- package/package.json +1 -1
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
|
-
(
|
|
2522
|
-
restarts). [app-id] is an app ID or name, --view
|
|
2523
|
-
|
|
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
|
|
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));
|
package/dist/apps/gateway.js
CHANGED
|
@@ -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
|
|
259
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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)
|
|
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 = !
|
|
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 (!
|
|
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,
|
|
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
|
|
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
|
|
919
|
+
const session = entry && this.session(req, entry.view.id);
|
|
884
920
|
const display = entry?.target.kind === "display";
|
|
885
|
-
if (!entry || !
|
|
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,
|
|
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,
|
|
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) {
|
package/dist/apps/service.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/apps/styles.css
CHANGED
|
@@ -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
|
package/dist/apps/types.js
CHANGED
|
@@ -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) =>
|
|
38
|
-
|
|
39
|
-
|
|
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