@bivy/bivy 0.17.2-staging.1 → 0.17.2-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/dist/apps/gateway.js +31 -12
- package/dist/apps/remote-preview.js +5 -0
- package/dist/apps/service.js +13 -0
- package/dist/apps/styles.css +3 -1
- package/dist/controllers/app-commands.js +2 -0
- package/dist/protocol/client-command-routes.js +1 -1
- package/dist/protocol/client-command-schemas.js +2 -0
- package/package.json +1 -1
package/dist/apps/gateway.js
CHANGED
|
@@ -16,6 +16,8 @@ const COOKIE = "__Host-bivy-preview";
|
|
|
16
16
|
const OPEN_PATH = "/__bivy/open";
|
|
17
17
|
const REDEEM_PATH = "/__bivy/redeem";
|
|
18
18
|
const HOUR = 60 * 60_000;
|
|
19
|
+
/** Copied links are reusable until revoked, capped so a forgotten one lapses. */
|
|
20
|
+
export const SHARE_TTL = 24 * HOUR;
|
|
19
21
|
const HOP = new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade"]);
|
|
20
22
|
/** Separate per-app origins are mandatory. TLS terminates at the deployment's
|
|
21
23
|
* preview ingress. Delivery reaches only this gateway, never the node API. */
|
|
@@ -85,9 +87,7 @@ export class AppGateway {
|
|
|
85
87
|
origin(id) { return this.template.replace("{app}", id); }
|
|
86
88
|
shellOrigin(id) { return this.template.replace("{app}", `view-${id}`); }
|
|
87
89
|
open(id, returnTo) {
|
|
88
|
-
const entry = this.
|
|
89
|
-
if (entry?.view.kind !== "web")
|
|
90
|
-
throw new Error("Web view not found.");
|
|
90
|
+
const entry = this.requireWeb(id);
|
|
91
91
|
if (returnTo) {
|
|
92
92
|
const url = new URL(returnTo);
|
|
93
93
|
const safeScheme = url.protocol === "https:" || (url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname));
|
|
@@ -97,13 +97,15 @@ export class AppGateway {
|
|
|
97
97
|
throw new Error("Invalid return-to-chat URL.");
|
|
98
98
|
returnTo = url.href;
|
|
99
99
|
}
|
|
100
|
-
this.sweep();
|
|
101
|
-
if (this.tickets.size >= 500 || this.sessions.size >= 500)
|
|
102
|
-
throw new Error("Too many preview grants. Try again later.");
|
|
103
|
-
const ticket = randomBytes(32).toString("hex");
|
|
104
|
-
this.tickets.set(ticket, { appId: id, expires: Date.now() + 60_000, returnTo });
|
|
105
100
|
// Fragment never reaches reverse-proxy access logs or the application.
|
|
106
|
-
return `${this.shellOrigin(id)}${OPEN_PATH}#${
|
|
101
|
+
return `${this.shellOrigin(id)}${OPEN_PATH}#${this.grant({ appId: id, expires: Date.now() + 60_000, returnTo })}`;
|
|
102
|
+
}
|
|
103
|
+
/** A reusable link straight to the app origin (no shell frame), so it opens
|
|
104
|
+
* in any browser and can be inspected. Valid until revoked or SHARE_TTL. */
|
|
105
|
+
share(id) {
|
|
106
|
+
this.requireWeb(id);
|
|
107
|
+
const expiresAt = Date.now() + SHARE_TTL;
|
|
108
|
+
return { url: `${this.origin(id)}${OPEN_PATH}#${this.grant({ appId: id, expires: expiresAt, reusable: true })}`, expiresAt };
|
|
107
109
|
}
|
|
108
110
|
revoke(id) {
|
|
109
111
|
for (const map of [this.tickets, this.sessions])
|
|
@@ -122,6 +124,20 @@ export class AppGateway {
|
|
|
122
124
|
this.server.close();
|
|
123
125
|
this.server.closeAllConnections();
|
|
124
126
|
}
|
|
127
|
+
requireWeb(id) {
|
|
128
|
+
const entry = this.registry.getView(id);
|
|
129
|
+
if (entry?.view.kind !== "web")
|
|
130
|
+
throw new Error("Web view not found.");
|
|
131
|
+
return entry;
|
|
132
|
+
}
|
|
133
|
+
grant(grant) {
|
|
134
|
+
this.sweep();
|
|
135
|
+
if (this.tickets.size >= 500 || this.sessions.size >= 500)
|
|
136
|
+
throw new Error("Too many preview grants. Try again later.");
|
|
137
|
+
const ticket = randomBytes(32).toString("hex");
|
|
138
|
+
this.tickets.set(ticket, grant);
|
|
139
|
+
return ticket;
|
|
140
|
+
}
|
|
125
141
|
sweep() {
|
|
126
142
|
for (const map of [this.tickets, this.sessions])
|
|
127
143
|
for (const [key, grant] of map)
|
|
@@ -247,10 +263,13 @@ export class AppGateway {
|
|
|
247
263
|
res.end();
|
|
248
264
|
return;
|
|
249
265
|
}
|
|
250
|
-
|
|
266
|
+
if (!grant.reusable)
|
|
267
|
+
this.tickets.delete(body);
|
|
268
|
+
// A browser session never outlives the link that created it.
|
|
269
|
+
const expires = Math.min(Date.now() + HOUR, grant.expires);
|
|
251
270
|
const token = randomBytes(32).toString("hex");
|
|
252
|
-
this.sessions.set(token, { appId: id, expires
|
|
253
|
-
res.setHeader("Set-Cookie", `${COOKIE}=${token}; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age
|
|
271
|
+
this.sessions.set(token, { appId: id, expires });
|
|
272
|
+
res.setHeader("Set-Cookie", `${COOKIE}=${token}; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=${Math.max(1, Math.floor((expires - Date.now()) / 1000))}`);
|
|
254
273
|
res.writeHead(204);
|
|
255
274
|
res.end();
|
|
256
275
|
return;
|
|
@@ -59,6 +59,11 @@ export class RemotePreview {
|
|
|
59
59
|
throw new Error("Preview delivery is unavailable. Reconnect the machine and try again.");
|
|
60
60
|
return this.gateway.open(id, returnTo);
|
|
61
61
|
}
|
|
62
|
+
share(id) {
|
|
63
|
+
if (!this.available)
|
|
64
|
+
throw new Error("Preview delivery is unavailable. Reconnect the machine and try again.");
|
|
65
|
+
return this.gateway.share(id);
|
|
66
|
+
}
|
|
62
67
|
revoke(id) { this.gateway?.revoke(id); }
|
|
63
68
|
disconnect() {
|
|
64
69
|
this.online = false;
|
package/dist/apps/service.js
CHANGED
|
@@ -48,6 +48,19 @@ export class AppService {
|
|
|
48
48
|
throw error;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
share(sessionId, appId, viewId) {
|
|
52
|
+
if (this.registry.requireView(sessionId, appId, viewId).view.kind !== "web")
|
|
53
|
+
throw new Error("Only web views have preview links.");
|
|
54
|
+
if (!this.gateway)
|
|
55
|
+
throw new Error("Bivy's preview service is unavailable on this connection.");
|
|
56
|
+
return this.gateway.share(viewId);
|
|
57
|
+
}
|
|
58
|
+
/** Ends every link, browser session and open connection for one view; the app stays. */
|
|
59
|
+
revoke(sessionId, appId, viewId) {
|
|
60
|
+
this.registry.requireView(sessionId, appId, viewId);
|
|
61
|
+
this.gateway?.revoke(viewId);
|
|
62
|
+
return { ok: true };
|
|
63
|
+
}
|
|
51
64
|
remove(sessionId, appId) {
|
|
52
65
|
const app = this.registry.require(sessionId, appId);
|
|
53
66
|
this.registry.remove(sessionId, appId);
|
package/dist/apps/styles.css
CHANGED
|
@@ -2954,7 +2954,9 @@ select.picker-search { appearance: auto; }
|
|
|
2954
2954
|
.app-views-heading { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
|
|
2955
2955
|
.app-views-heading .artifact-name, .app-message .artifact-name { white-space: normal; overflow-wrap: anywhere; }
|
|
2956
2956
|
.app-view-row { flex-wrap: wrap; }
|
|
2957
|
-
.app-
|
|
2957
|
+
.app-views-heading > .btn, .app-view-actions .btn { min-block-size: var(--space-7); flex-shrink: 0; }
|
|
2958
|
+
.app-view-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); margin-inline-start: auto; }
|
|
2959
|
+
.app-view-notice { display: flex; flex-direction: column; gap: var(--space-2); flex-basis: 100%; min-width: 0; }
|
|
2958
2960
|
.app-terminal-status { padding: var(--space-2) var(--space-4); margin: 0; color: var(--muted); font-size: var(--text-sm); }
|
|
2959
2961
|
.app-view-command { overflow-wrap: anywhere; white-space: pre-wrap; font-size: var(--text-xs); color: var(--muted); }
|
|
2960
2962
|
.app-view-row .artifact-main { flex-basis: 60%; }
|
|
@@ -11,6 +11,8 @@ export function createAppCommands(service, workspaceFor, published = () => { })
|
|
|
11
11
|
return { app };
|
|
12
12
|
},
|
|
13
13
|
"apps.open": (msg) => service.open(String(msg.sessionId), String(msg.appId), String(msg.viewId), typeof msg.returnTo === "string" ? msg.returnTo : undefined),
|
|
14
|
+
"apps.share": (msg) => service.share(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
|
|
15
|
+
"apps.revoke": (msg) => service.revoke(String(msg.sessionId), String(msg.appId), String(msg.viewId)),
|
|
14
16
|
"apps.remove": (msg) => { service.remove(String(msg.sessionId), String(msg.appId)); return { ok: true }; },
|
|
15
17
|
};
|
|
16
18
|
return Object.fromEntries(Object.entries(operations).map(([kind, execute]) => [kind, async (msg, ctx) => {
|
|
@@ -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", "open", "remove"].map((action) => ({ method: "post", path: `/api/apps/${action}`, kind: `apps.${action}` })),
|
|
6
|
+
...["list", "publish", "open", "share", "revoke", "remove"].map((action) => ({ method: "post", path: `/api/apps/${action}`, kind: `apps.${action}` })),
|
|
7
7
|
{ method: "post", path: "/api/auth/credentials/native-preview", kind: "credentials.native.preview" },
|
|
8
8
|
{ method: "post", path: "/api/auth/credentials/native-import", kind: "credentials.native.import" },
|
|
9
9
|
{ method: "post", path: "/api/session/pause", kind: "session.pause" },
|
|
@@ -11,6 +11,8 @@ export const CLIENT_COMMAND_SCHEMAS = {
|
|
|
11
11
|
"apps.list": Type.Object({ ...request, ...session }),
|
|
12
12
|
"apps.publish": Type.Object({ ...request, ...session, manifest: Type.Unknown() }),
|
|
13
13
|
"apps.open": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String(), returnTo: Type.Optional(Type.String({ maxLength: 2048 })) }),
|
|
14
|
+
"apps.share": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String() }),
|
|
15
|
+
"apps.revoke": Type.Object({ ...request, ...session, appId: Type.String(), viewId: Type.String() }),
|
|
14
16
|
"apps.remove": Type.Object({ ...request, ...session, appId: Type.String() }),
|
|
15
17
|
"credentials.native.preview": Type.Object({ ...request, label: Type.Optional(Type.String({ maxLength: 100 })) }),
|
|
16
18
|
"credentials.native.import": Type.Object({
|
package/package.json
CHANGED