@saadnvd1/agent-os 0.3.775 → 0.3.777
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/app/api/live-shares/[id]/route.ts +72 -0
- package/app/api/live-shares/route.test.ts +45 -0
- package/app/api/live-shares/route.ts +40 -0
- package/app/page.tsx +5 -0
- package/components/Chat/ChatPanel.tsx +6 -0
- package/components/Chat/EventRow.tsx +6 -0
- package/components/Live/GuestPromptCard.tsx +64 -0
- package/components/Live/InviteDialog.tsx +225 -0
- package/components/Live/LiveChip.tsx +109 -0
- package/components/Live/LivePanel.tsx +81 -0
- package/components/Live/LobbyCard.tsx +64 -0
- package/components/Pane/MobileTabBar.tsx +8 -0
- package/components/Pane/PaneMenu.tsx +10 -0
- package/components/SessionList/SessionRowMenu.tsx +6 -0
- package/components/Shares/SharedLinksPanel.tsx +51 -0
- package/data/live/index.ts +2 -0
- package/data/live/keys.ts +4 -0
- package/data/live/queries.ts +81 -0
- package/lib/chat/events.ts +4 -1
- package/lib/chat/registry.ts +10 -0
- package/lib/connect/control-api.ts +44 -0
- package/lib/db/migrations/20261029T1400_live_shares.ts +42 -0
- package/lib/live/clean.ts +54 -0
- package/lib/live/frames.ts +132 -0
- package/lib/live/inbox.ts +225 -0
- package/lib/live/publisher.ts +84 -0
- package/lib/live/ring.ts +61 -0
- package/lib/live/runtime.ts +51 -0
- package/lib/live/service.ts +318 -0
- package/lib/live/socket.ts +293 -0
- package/lib/live/store.ts +149 -0
- package/lib/live/types.ts +39 -0
- package/lib/share/connect.ts +24 -0
- package/lib/share/redact.ts +23 -1
- package/lib/share/secret-patterns.ts +77 -0
- package/lib/sidebar/row-menu.ts +2 -0
- package/package.json +1 -1
- package/server.ts +2 -0
- package/stores/liveUi.ts +17 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { db } from "../db";
|
|
2
|
+
|
|
3
|
+
export interface LiveShareRow {
|
|
4
|
+
id: string;
|
|
5
|
+
session_id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
url: string;
|
|
8
|
+
ends_with_session: number;
|
|
9
|
+
max_guests: number;
|
|
10
|
+
created_at: string;
|
|
11
|
+
expires_at: string | null;
|
|
12
|
+
ended_at: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type LogKind =
|
|
16
|
+
| "join_request"
|
|
17
|
+
| "admit"
|
|
18
|
+
| "deny"
|
|
19
|
+
| "kick"
|
|
20
|
+
| "suggestion"
|
|
21
|
+
| "sent"
|
|
22
|
+
| "dismissed";
|
|
23
|
+
|
|
24
|
+
export interface LogRow {
|
|
25
|
+
id: number;
|
|
26
|
+
share_id: string;
|
|
27
|
+
at: number;
|
|
28
|
+
kind: LogKind;
|
|
29
|
+
guest_id: string | null;
|
|
30
|
+
guest_name: string | null;
|
|
31
|
+
ref: string | null;
|
|
32
|
+
text: string | null;
|
|
33
|
+
sent_text: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function insertLiveShare(row: LiveShareRow): void {
|
|
37
|
+
db.prepare(
|
|
38
|
+
`INSERT INTO live_shares
|
|
39
|
+
(id, session_id, title, url, ends_with_session, max_guests, created_at, expires_at, ended_at)
|
|
40
|
+
VALUES (@id, @session_id, @title, @url, @ends_with_session, @max_guests, @created_at, @expires_at, @ended_at)`
|
|
41
|
+
).run(row);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getLiveShare(id: string): LiveShareRow | undefined {
|
|
45
|
+
return db.prepare(`SELECT * FROM live_shares WHERE id = ?`).get(id) as
|
|
46
|
+
LiveShareRow | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Shares this machine still holds: not ended, not past their time. */
|
|
50
|
+
export function activeLiveShares(now = Date.now()): LiveShareRow[] {
|
|
51
|
+
return (
|
|
52
|
+
db
|
|
53
|
+
.prepare(
|
|
54
|
+
`SELECT * FROM live_shares WHERE ended_at IS NULL ORDER BY created_at, rowid`
|
|
55
|
+
)
|
|
56
|
+
.all() as LiveShareRow[]
|
|
57
|
+
).filter((s) => !s.expires_at || Date.parse(s.expires_at) > now);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function activeLiveShareFor(
|
|
61
|
+
sessionId: string,
|
|
62
|
+
now = Date.now()
|
|
63
|
+
): LiveShareRow | undefined {
|
|
64
|
+
return activeLiveShares(now).find((s) => s.session_id === sessionId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function markLiveEnded(id: string): void {
|
|
68
|
+
db.prepare(
|
|
69
|
+
`UPDATE live_shares SET ended_at = COALESCE(ended_at, ?) WHERE id = ?`
|
|
70
|
+
).run(new Date().toISOString(), id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function logLive(
|
|
74
|
+
row: Omit<LogRow, "id" | "at"> & { at?: number }
|
|
75
|
+
): void {
|
|
76
|
+
db.prepare(
|
|
77
|
+
`INSERT INTO live_share_log (share_id, at, kind, guest_id, guest_name, ref, text, sent_text)
|
|
78
|
+
VALUES (@share_id, @at, @kind, @guest_id, @guest_name, @ref, @text, @sent_text)`
|
|
79
|
+
).run({ ...row, at: row.at ?? Date.now() });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function listLog(shareId: string): LogRow[] {
|
|
83
|
+
return db
|
|
84
|
+
.prepare(`SELECT * FROM live_share_log WHERE share_id = ? ORDER BY id`)
|
|
85
|
+
.all(shareId) as LogRow[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface OpenSuggestion {
|
|
89
|
+
id: string;
|
|
90
|
+
guestId: string;
|
|
91
|
+
guestName: string;
|
|
92
|
+
text: string;
|
|
93
|
+
at: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Suggestions with no sent or dismissed line yet, oldest first. */
|
|
97
|
+
export function openSuggestions(shareId: string): OpenSuggestion[] {
|
|
98
|
+
const rows = db
|
|
99
|
+
.prepare(
|
|
100
|
+
`SELECT s.ref, s.guest_id, s.guest_name, s.text, s.at FROM live_share_log s
|
|
101
|
+
WHERE s.share_id = ? AND s.kind = 'suggestion' AND NOT EXISTS (
|
|
102
|
+
SELECT 1 FROM live_share_log d
|
|
103
|
+
WHERE d.share_id = s.share_id AND d.ref = s.ref AND d.kind IN ('sent', 'dismissed'))
|
|
104
|
+
ORDER BY s.id`
|
|
105
|
+
)
|
|
106
|
+
.all(shareId) as {
|
|
107
|
+
ref: string;
|
|
108
|
+
guest_id: string;
|
|
109
|
+
guest_name: string;
|
|
110
|
+
text: string;
|
|
111
|
+
at: number;
|
|
112
|
+
}[];
|
|
113
|
+
return rows.map((r) => ({
|
|
114
|
+
id: r.ref,
|
|
115
|
+
guestId: r.guest_id,
|
|
116
|
+
guestName: r.guest_name,
|
|
117
|
+
text: r.text,
|
|
118
|
+
at: r.at,
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Guests this machine admitted and has not removed since. */
|
|
123
|
+
export function admittedGuestIds(shareId: string): Set<string> {
|
|
124
|
+
const out = new Set<string>();
|
|
125
|
+
for (const l of listLog(shareId)) {
|
|
126
|
+
if (!l.guest_id) continue;
|
|
127
|
+
if (l.kind === "admit") out.add(l.guest_id);
|
|
128
|
+
else if (l.kind === "kick" || l.kind === "deny") out.delete(l.guest_id);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Whether a suggestion with this id was ever logged for the share. */
|
|
134
|
+
export function hasSuggestion(shareId: string, ref: string): boolean {
|
|
135
|
+
return !!db
|
|
136
|
+
.prepare(
|
|
137
|
+
`SELECT 1 FROM live_share_log WHERE share_id = ? AND kind = 'suggestion' AND ref = ? LIMIT 1`
|
|
138
|
+
)
|
|
139
|
+
.get(shareId, ref);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Whether a join request from this guest was already logged. */
|
|
143
|
+
export function hasJoinRequest(shareId: string, guestId: string): boolean {
|
|
144
|
+
return !!db
|
|
145
|
+
.prepare(
|
|
146
|
+
`SELECT 1 FROM live_share_log WHERE share_id = ? AND kind = 'join_request' AND guest_id = ? LIMIT 1`
|
|
147
|
+
)
|
|
148
|
+
.get(shareId, guestId);
|
|
149
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// What the host's screens show of a live share (served by /api/live-shares).
|
|
2
|
+
export interface LiveView {
|
|
3
|
+
id: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
title: string;
|
|
6
|
+
url: string;
|
|
7
|
+
expiresAt: string | null;
|
|
8
|
+
endsWithSession: boolean;
|
|
9
|
+
maxGuests: number;
|
|
10
|
+
connected: boolean;
|
|
11
|
+
notice: string | null;
|
|
12
|
+
guests: {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
status: "pending" | "admitted";
|
|
16
|
+
online: boolean;
|
|
17
|
+
/** Another guest here has the same name. */
|
|
18
|
+
duplicateName: boolean;
|
|
19
|
+
}[];
|
|
20
|
+
suggestions: {
|
|
21
|
+
id: string;
|
|
22
|
+
guestId: string;
|
|
23
|
+
guestName: string;
|
|
24
|
+
text: string;
|
|
25
|
+
at: number;
|
|
26
|
+
}[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type LiveExpiry = "1h" | "session";
|
|
30
|
+
|
|
31
|
+
export const EXPIRY_LABEL: Record<LiveExpiry, string> = {
|
|
32
|
+
"1h": "1 hour",
|
|
33
|
+
session: "Until the session ends",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface LiveResponse {
|
|
37
|
+
linked: boolean;
|
|
38
|
+
shares: LiveView[];
|
|
39
|
+
}
|
package/lib/share/connect.ts
CHANGED
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
clean,
|
|
7
7
|
listShares,
|
|
8
8
|
replaceShare,
|
|
9
|
+
createLiveShare,
|
|
9
10
|
revokeShare,
|
|
10
11
|
uploadShare,
|
|
12
|
+
type LiveExpiry,
|
|
11
13
|
type RemoteShare,
|
|
12
14
|
type ShareExpiry,
|
|
13
15
|
} from "@/lib/connect/control-api";
|
|
@@ -40,6 +42,16 @@ function explain(err: unknown): never {
|
|
|
40
42
|
`Connect refused it as too big or over your quota: ${clean(err.message, 120)}`,
|
|
41
43
|
413
|
|
42
44
|
);
|
|
45
|
+
if (err.status === 402)
|
|
46
|
+
throw new ShareError(
|
|
47
|
+
clean(
|
|
48
|
+
typeof err.body.error === "string"
|
|
49
|
+
? err.body.error
|
|
50
|
+
: "Your plan's live share limit is reached.",
|
|
51
|
+
160
|
|
52
|
+
),
|
|
53
|
+
402
|
|
54
|
+
);
|
|
43
55
|
if (err.status === 429)
|
|
44
56
|
throw new ShareError(
|
|
45
57
|
"Too many shares just now. Try again in a few minutes.",
|
|
@@ -95,3 +107,15 @@ export async function revokeRemote(id: string): Promise<void> {
|
|
|
95
107
|
explain(err);
|
|
96
108
|
}
|
|
97
109
|
}
|
|
110
|
+
|
|
111
|
+
export async function createLiveRemote(share: {
|
|
112
|
+
title: string;
|
|
113
|
+
expiresIn: LiveExpiry;
|
|
114
|
+
}) {
|
|
115
|
+
const { config, machineKey } = files();
|
|
116
|
+
try {
|
|
117
|
+
return await createLiveShare(config, machineKey, share);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
explain(err);
|
|
120
|
+
}
|
|
121
|
+
}
|
package/lib/share/redact.ts
CHANGED
|
@@ -18,6 +18,15 @@ import {
|
|
|
18
18
|
BASE64_RUN,
|
|
19
19
|
looksRandom,
|
|
20
20
|
} from "@/lib/orchestrator/untrusted";
|
|
21
|
+
import {
|
|
22
|
+
BASIC_AUTH,
|
|
23
|
+
CLI_FLAG,
|
|
24
|
+
DATA_URI,
|
|
25
|
+
KEY_VALUE,
|
|
26
|
+
MYSQL_P,
|
|
27
|
+
TOKEN_RUN,
|
|
28
|
+
isHighEntropyToken,
|
|
29
|
+
} from "./secret-patterns";
|
|
21
30
|
import { HOME } from "@/lib/session-audit/normalize";
|
|
22
31
|
|
|
23
32
|
export type Kind = "secret" | "path" | "ip" | "host" | "email";
|
|
@@ -59,7 +68,8 @@ const SECRET = "[secret]";
|
|
|
59
68
|
const BEARER = /(\bbearer\s+)(?!\[secret\])[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
60
69
|
const INLINE_ASSIGNMENT =
|
|
61
70
|
/(\b[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|PWD|CREDENTIALS?|AUTH)[A-Z0-9_]*=)(?!\[secret\])[^\s'"]+/g;
|
|
62
|
-
|
|
71
|
+
// Up to 64 hex characters is a git sha or a hash, not a key.
|
|
72
|
+
const LONG_HEX = /\b[a-fA-F0-9]{65,}\b/g;
|
|
63
73
|
|
|
64
74
|
const IPV4 =
|
|
65
75
|
/(?<![\w.])(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)(?![\w.]*\w)/g;
|
|
@@ -104,6 +114,10 @@ export function redactForShare(
|
|
|
104
114
|
let t = input;
|
|
105
115
|
const on = (k: Kind) => !opts.only || opts.only.includes(k);
|
|
106
116
|
|
|
117
|
+
// Inline base64 (images) is content, not a credential: set aside, put back.
|
|
118
|
+
const uris: string[] = [];
|
|
119
|
+
t = t.replace(DATA_URI, (m) => `\u0000${uris.push(m) - 1}\u0000`);
|
|
120
|
+
|
|
107
121
|
// 1. Credentials by shape.
|
|
108
122
|
t = sub(t, PRIVATE_KEY_BLOCK, SECRET, hits, "secret");
|
|
109
123
|
t = sub(
|
|
@@ -132,9 +146,16 @@ export function redactForShare(
|
|
|
132
146
|
"secret",
|
|
133
147
|
(m) => m.endsWith(SECRET)
|
|
134
148
|
);
|
|
149
|
+
const keepValue = (_m: string, k: string, v: string) =>
|
|
150
|
+
`${k}${/^["']/.test(v) ? v[0] + SECRET + v[0] : SECRET}`;
|
|
151
|
+
t = sub(t, KEY_VALUE, keepValue, hits, "secret");
|
|
152
|
+
t = sub(t, CLI_FLAG, keepValue, hits, "secret");
|
|
153
|
+
t = sub(t, MYSQL_P, (_m, k) => `${k}${SECRET}`, hits, "secret");
|
|
154
|
+
t = sub(t, BASIC_AUTH, (_m, k) => `${k}${SECRET}`, hits, "secret");
|
|
135
155
|
for (const re of SECRET_TOKENS) t = sub(t, re, SECRET, hits, "secret");
|
|
136
156
|
t = sub(t, LONG_HEX, SECRET, hits, "secret");
|
|
137
157
|
t = sub(t, BASE64_RUN, SECRET, hits, "secret", (m) => !looksRandom(m));
|
|
158
|
+
t = sub(t, TOKEN_RUN, SECRET, hits, "secret", (m) => !isHighEntropyToken(m));
|
|
138
159
|
|
|
139
160
|
// 2. This machine's own secrets, wherever they appear.
|
|
140
161
|
for (const value of ctx.secrets) {
|
|
@@ -192,5 +213,6 @@ export function redactForShare(
|
|
|
192
213
|
// 6. Email addresses.
|
|
193
214
|
if (on("email")) t = sub(t, EMAIL, "[email]", hits, "email");
|
|
194
215
|
|
|
216
|
+
t = t.replace(/\u0000(\d+)\u0000/g, (_m, i) => uris[Number(i)]);
|
|
195
217
|
return { text: t, hits };
|
|
196
218
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic credential shapes for redactForShare: key/value secrets in any
|
|
3
|
+
* syntax, Basic auth headers, mysql -p, and high-entropy tokens. Pure data
|
|
4
|
+
* and predicates; redact.ts applies them and counts the hits.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const WORDS =
|
|
8
|
+
"password|passwd|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|client[_-]?secret|auth|private[_-]?key";
|
|
9
|
+
|
|
10
|
+
// A value that is a type, a placeholder or a call, not a credential.
|
|
11
|
+
const NOT_A_VALUE =
|
|
12
|
+
"(?![\"']?\\[secret\\])" +
|
|
13
|
+
"(?!(?:string|number|boolean|any|unknown|null|undefined|true|false|none|void|required|optional)\\b)" +
|
|
14
|
+
"(?![$<{(\\[%]|\\.\\.\\.)(?![\\w.]+\\()";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `password=x`, `"api_key": "x"`, `token: x`, `export DB_PASSWORD=x`,
|
|
18
|
+
* `--password=x`. The key may carry any prefix (GITHUB_TOKEN) but must end
|
|
19
|
+
* in the word, so `author` and `max_tokens` stay. Group 1 is kept.
|
|
20
|
+
*/
|
|
21
|
+
export const KEY_VALUE = new RegExp(
|
|
22
|
+
`((?<![\\w.-])[\\w.-]*(?:${WORDS})["']?[ \\t]*[:=][ \\t]*)` +
|
|
23
|
+
`${NOT_A_VALUE}("[^"\\n]*"|'[^'\\n]*'|[^\\s,;'"&}\\])]+)`,
|
|
24
|
+
"gi"
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/** `--password value`, `--api-key value` (space separated). */
|
|
28
|
+
export const CLI_FLAG = new RegExp(
|
|
29
|
+
`(--[\\w-]*?(?:${WORDS})[ \\t]+)(?!-)${NOT_A_VALUE}("[^"\\n]*"|'[^'\\n]*'|\\S+)`,
|
|
30
|
+
"gi"
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* mysql-family `-pSECRET` (attached: `-p secret` would be a database name).
|
|
35
|
+
* psql's -p is a port, so it is left alone.
|
|
36
|
+
*/
|
|
37
|
+
export const MYSQL_P =
|
|
38
|
+
/(\b(?:mysql|mysqldump|mysqladmin|mariadb)\b[^\n|;&]*?\s-p)(?=\S)(\S+)/gi;
|
|
39
|
+
|
|
40
|
+
/** `Authorization: Basic <x>` (Bearer is handled in redact.ts). */
|
|
41
|
+
export const BASIC_AUTH =
|
|
42
|
+
/(\bauthorization["']?\s*[:=]\s*["']?basic\s+)[A-Za-z0-9+/=._~-]{4,}/gi;
|
|
43
|
+
|
|
44
|
+
/** A base64 data URI, such as an inline image. Kept as is: not a credential. */
|
|
45
|
+
export const DATA_URI =
|
|
46
|
+
/data:[\w.+-]+\/[\w.+-]+(?:;[\w=.-]+)*;base64,[A-Za-z0-9+/=_-]+/g;
|
|
47
|
+
|
|
48
|
+
/** Candidate runs for the entropy test. */
|
|
49
|
+
export const TOKEN_RUN = /(?<![A-Za-z0-9_\-+/=])[A-Za-z0-9_\-+/=]{32,}/g;
|
|
50
|
+
|
|
51
|
+
export const ENTROPY_MIN = 4.0;
|
|
52
|
+
|
|
53
|
+
export function shannon(s: string): number {
|
|
54
|
+
const counts = new Map<string, number>();
|
|
55
|
+
for (const c of s) counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
56
|
+
let h = 0;
|
|
57
|
+
for (const n of counts.values())
|
|
58
|
+
h -= (n / s.length) * Math.log2(n / s.length);
|
|
59
|
+
return h;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
63
|
+
|
|
64
|
+
function looksLikePath(s: string): boolean {
|
|
65
|
+
if (/^(?:\.{0,2}\/|~\/)/.test(s)) return true;
|
|
66
|
+
const parts = s.split("/");
|
|
67
|
+
return parts.length > 2 && parts.every((p) => p.length >= 2);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** True when a run is a random-looking token rather than prose, a hash or a path. */
|
|
71
|
+
export function isHighEntropyToken(s: string): boolean {
|
|
72
|
+
if (/^[0-9a-f]+$/i.test(s) && s.length <= 64) return false; // git sha, md5/sha256
|
|
73
|
+
if (UUID.test(s)) return false;
|
|
74
|
+
if (looksLikePath(s)) return false;
|
|
75
|
+
if (!/[a-z]/.test(s) || !/[A-Z]/.test(s) || !/[0-9]/.test(s)) return false;
|
|
76
|
+
return shannon(s) > ENTROPY_MIN;
|
|
77
|
+
}
|
package/lib/sidebar/row-menu.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type RowMenuItem =
|
|
|
17
17
|
| "copy-link"
|
|
18
18
|
| "share"
|
|
19
19
|
| "export"
|
|
20
|
+
| "invite-live"
|
|
20
21
|
| "fork"
|
|
21
22
|
| "fresh-start"
|
|
22
23
|
| "new-conversation"
|
|
@@ -127,6 +128,7 @@ export function rowMenu(input: RowMenuInput): RowMenuPlan {
|
|
|
127
128
|
...group("share", [
|
|
128
129
|
"copy-link",
|
|
129
130
|
s.view === "chat" && "share",
|
|
131
|
+
s.view === "chat" && "invite-live",
|
|
130
132
|
s.view === "chat" && "export",
|
|
131
133
|
]),
|
|
132
134
|
...group("restart", [
|
package/package.json
CHANGED
package/server.ts
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
import { lanEnabled } from "./lib/security/network-settings";
|
|
55
55
|
import { handlePeerRpc, isPeerRpc, rpcDeps } from "./lib/peer/rpc-server";
|
|
56
56
|
import { startConnect } from "./lib/connect/serve";
|
|
57
|
+
import { startLive } from "./lib/live/service";
|
|
57
58
|
import { serverPort } from "./lib/connect/config";
|
|
58
59
|
import { startUpdateChecks, updateChecksEnabled } from "./lib/updates/check";
|
|
59
60
|
import {
|
|
@@ -399,6 +400,7 @@ app.prepare().then(async () => {
|
|
|
399
400
|
? { hostname: () => null }
|
|
400
401
|
: startConnect({ onRequest, onUpgrade }, port);
|
|
401
402
|
policy.machineHost = () => connect.hostname();
|
|
403
|
+
if (!demo) startLive();
|
|
402
404
|
// HTTPS on the tailnet (its own port) for passkeys and other secure-origin APIs.
|
|
403
405
|
if (
|
|
404
406
|
!demo &&
|
package/stores/liveUi.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { proxy } from "valtio";
|
|
2
|
+
|
|
3
|
+
// The invite dialog: which chat it is open on, started from a menu.
|
|
4
|
+
export const liveUi = proxy<{
|
|
5
|
+
target: { sessionId: string; title: string } | null;
|
|
6
|
+
}>({
|
|
7
|
+
target: null,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const liveUiActions = {
|
|
11
|
+
open: (sessionId: string, title: string) => {
|
|
12
|
+
liveUi.target = { sessionId, title };
|
|
13
|
+
},
|
|
14
|
+
close: () => {
|
|
15
|
+
liveUi.target = null;
|
|
16
|
+
},
|
|
17
|
+
};
|