@volter/teams 0.2.10 → 0.2.12
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/checkout.mjs +29 -0
- package/event-client.mjs +54 -3
- package/http-client.d.ts +5 -1
- package/http-client.mjs +18 -5
- package/machine-door.mjs +22 -2
- package/package.json +9 -3
- package/redact/gitleaks-rules.json +1 -0
- package/redact.mjs +219 -0
- package/server/events.mjs +20 -0
- package/server/machine-events.mjs +3 -5
- package/server/published-events.mjs +3 -4
- package/session-follow.mjs +12 -1
- package/viewer/session-view.css +39 -0
- package/viewer/session-view.mjs +28 -28
package/server/events.mjs
CHANGED
|
@@ -15,3 +15,23 @@ export function sseRoom(res,bytes,{cap=SSE_BACKLOG_BYTES,patienceMs=SSE_PATIENCE
|
|
|
15
15
|
res.on('drain',drained);res.on('close',ended);
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
+
// Open event streams are counted per team install: a credential on one machine (a machine credential's own machine, or
|
|
19
|
+
// the enrolled machine of this team its request names in X-Supercode-Machine), else the credential with no machine.
|
|
20
|
+
// An install's connector holds one published-events stream that its local readers share (dispatchers, agents, apps on
|
|
21
|
+
// that machine read through its door), and its people open a few viewers beside it (session follows, a catalogue
|
|
22
|
+
// watch): one stream, one more while a dropped one is still being noticed, about five viewers, and headroom makes 10.
|
|
23
|
+
// A credential's streams over all its machines stop at 40, so a leaked token cannot hold the server's connections.
|
|
24
|
+
export const STREAMS_PER_INSTALL=10, STREAMS_PER_CREDENTIAL=40;
|
|
25
|
+
// A slot frees only when another stream of the install closes; a refused reader asks again no sooner than this.
|
|
26
|
+
export const STREAM_RETRY_SECONDS=30;
|
|
27
|
+
/** Count one open stream for `auth`'s install, refusing past the limits; resolves the release (idempotent). */
|
|
28
|
+
export async function holdStream(api,auth,req){
|
|
29
|
+
const named=String(req.headers['x-supercode-machine']??'');
|
|
30
|
+
let machine=auth.kind==='machine'?auth.machine_id:'';
|
|
31
|
+
if(!machine&&/^mach_[A-Za-z0-9_-]{1,64}$/.test(named)&&await api.store.one('SELECT 1 FROM machines WHERE team_id=? AND id=? AND revoked_at IS NULL',[auth.team_id,named]))machine=named;
|
|
32
|
+
const keys=[`install:${auth.credential_id}\0${machine}`,`credential:${auth.credential_id}`],limits=[STREAMS_PER_INSTALL,STREAMS_PER_CREDENTIAL];
|
|
33
|
+
if(keys.some((key,at)=>(api.openStreams.get(key)??0)>=limits[at]))throw Object.assign(new Error(`too many open event streams for this ${machine?'machine':'credential'}`),{status:429,code:'rate_limited',details:{retry_after_seconds:STREAM_RETRY_SECONDS,streams_per_install:STREAMS_PER_INSTALL,machine:machine||null}});
|
|
34
|
+
for(const key of keys)api.openStreams.set(key,(api.openStreams.get(key)??0)+1);
|
|
35
|
+
let held=true;
|
|
36
|
+
return ()=>{if(!held)return;held=false;for(const key of keys){const next=(api.openStreams.get(key)??1)-1;if(next>0)api.openStreams.set(key,next);else api.openStreams.delete(key);}};
|
|
37
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// A retained machine transition log for Teams mailbox subscriptions.
|
|
2
|
-
import { writeSse } from './events.mjs';
|
|
2
|
+
import { writeSse, holdStream } from './events.mjs';
|
|
3
3
|
|
|
4
4
|
export function machineTransition(api, team, machine, linked) {
|
|
5
5
|
// Serialize the read/change/event across asynchronous store calls. A replaced
|
|
@@ -37,11 +37,9 @@ export async function reconcileMachinePresence(api) {
|
|
|
37
37
|
export async function machineEventStream({api,req,res,auth,url,requestId,fail}) {
|
|
38
38
|
const after = url.searchParams.get('after') ?? req.headers['last-event-id'];
|
|
39
39
|
if (after != null && (!/^\d+$/.test(after) || !Number.isSafeInteger(Number(after)))) fail(400,'invalid_request','after must be a machine event cursor');
|
|
40
|
-
const
|
|
41
|
-
if (count >= 10) fail(429,'rate_limited','too many open streams');
|
|
42
|
-
api.openStreams.set(auth.credential_id,count+1);
|
|
40
|
+
const release = await holdStream(api,auth,req);
|
|
43
41
|
let closed=false, cursor=after==null?null:Number(after), work=Promise.resolve();
|
|
44
|
-
const close=()=>{ if(closed)return;closed=true;clearInterval(timer);unsubscribe();req.off('close',close);api.events.off('shutdown',close);
|
|
42
|
+
const close=()=>{ if(closed)return;closed=true;clearInterval(timer);unsubscribe();req.off('close',close);api.events.off('shutdown',close);release();res.end(); };
|
|
45
43
|
const write=event=>{if(closed)return;if(res.writableLength>1024*1024){close();return;}writeSse(res,event);};
|
|
46
44
|
const drain=async()=>{
|
|
47
45
|
if(closed)return;
|
|
@@ -3,7 +3,7 @@ import { appAuthentication } from './apps.mjs';
|
|
|
3
3
|
import { isAdmin } from './auth.mjs';
|
|
4
4
|
import { exact } from './request-json.mjs';
|
|
5
5
|
import { canonicalJson } from './wire.mjs';
|
|
6
|
-
import { sseRoom, writeSse } from './events.mjs';
|
|
6
|
+
import { sseRoom, writeSse, holdStream } from './events.mjs';
|
|
7
7
|
import { APP_EVENTS, PUBLISHABLE_EVENTS, eventReadAction } from './event-catalogue.mjs';
|
|
8
8
|
import { EVENT_DATA_LIMIT } from '../event-limits.mjs';
|
|
9
9
|
|
|
@@ -159,10 +159,9 @@ async function eventStream({api,req,res,auth,url,requestId}){
|
|
|
159
159
|
if([...url.searchParams.keys()].some(key=>!['after','source','kinds'].includes(key))||query.kinds?.some(kind=>!APP_EVENTS.includes(kind)))fail(400,'invalid_request','invalid event subscription');
|
|
160
160
|
auth=await liveAuth(api,auth);let codec=cursorCodec(api,auth,query);
|
|
161
161
|
const after=url.searchParams.get('after')??req.headers['last-event-id'];let cursor=after==null?null:codec.decode(after);
|
|
162
|
-
const
|
|
163
|
-
api.openStreams.set(auth.credential_id,count+1);
|
|
162
|
+
const release=await holdStream(api,auth,req);
|
|
164
163
|
let closed=false,running=null,dirty=false;
|
|
165
|
-
const close=()=>{if(closed)return;closed=true;clearInterval(timer);unsubscribe();req.off('close',close);api.events.off('shutdown',close);
|
|
164
|
+
const close=()=>{if(closed)return;closed=true;clearInterval(timer);unsubscribe();req.off('close',close);api.events.off('shutdown',close);release();res.end();};
|
|
166
165
|
const terminal=(type,reason)=>{if(!closed)writeSse(res,{type,reason,terminal:true});close();};
|
|
167
166
|
const write=async event=>{if(closed)return false;const room=await sseRoom(res,Buffer.byteLength(JSON.stringify(event)));if(closed||room==='closed')return false;if(room==='slow'){terminal('resync_required','slow_reader');return false;}writeSse(res,event);return true;};
|
|
168
167
|
const readable=async event=>{
|
package/session-follow.mjs
CHANGED
|
@@ -163,6 +163,16 @@ const automationImage = seed => `https://brand.volter.ai/bot/svg?seed=${encodeUR
|
|
|
163
163
|
* may draw its own faces; `personImage`/`agentImage` choose the default pictures. One author's run within five minutes
|
|
164
164
|
* is `continued`, and each entry has a short local `timeLabel`.
|
|
165
165
|
*/
|
|
166
|
+
/**
|
|
167
|
+
* A Room member's words, without the envelope Teams types ahead of them into the session ('[Room "<name>" ("room_…");
|
|
168
|
+
* sender "<name>" ("prn_…")] ' then the words). Only that exact quote is taken off, and only when the sender it names is
|
|
169
|
+
* the principal recorded as typing the turn; any other text, a forged or quoted envelope included, is shown as typed.
|
|
170
|
+
*/
|
|
171
|
+
function roomWords(text, principal) {
|
|
172
|
+
const m = /^\s*\[Room "(?:[^"\\]|\\.)*" \("room_[A-Za-z0-9]+"\); sender "(?:[^"\\]|\\.)*" \("(prn_[A-Za-z0-9]+)"\)\]\s*/.exec(String(text ?? ''));
|
|
173
|
+
return m && principal && m[1] === principal ? String(text).slice(m[0].length) : text;
|
|
174
|
+
}
|
|
175
|
+
|
|
166
176
|
export function withPeople(transcript, messages, { agentName = null, sessionId = '', personImage: person = personImage, agentImage: agent = agentImage, locale = [] } = {}) {
|
|
167
177
|
const byTime = new Map();
|
|
168
178
|
for (const message of messages) {
|
|
@@ -204,7 +214,8 @@ export function withPeople(transcript, messages, { agentName = null, sessionId =
|
|
|
204
214
|
const seed = name ? (metadata.source_principal_id || name) : '';
|
|
205
215
|
const via = typeof metadata.source_via === 'string' ? metadata.source_via.trim() : '';
|
|
206
216
|
const source = ({ room: 'Room', app: 'App', terminal: 'Terminal', api: 'API' })[via] || via || 'Session';
|
|
207
|
-
|
|
217
|
+
const text = via === 'room' ? roomWords(entry.text, metadata.source_principal_id) : entry.text;
|
|
218
|
+
return { ...entry, text, actor: { ...(seed ? { id: seed } : {}), name: name ?? 'Anonymous', anonymous: !name, source, image: person(seed), kind: 'person' } };
|
|
208
219
|
});
|
|
209
220
|
let last = null;
|
|
210
221
|
return people.map((entry) => {
|
package/viewer/session-view.css
CHANGED
|
@@ -669,6 +669,8 @@
|
|
|
669
669
|
.scui-configuration-value-input { flex:1; min-width:calc(120px * var(--scui-scaling)); width:100%; }
|
|
670
670
|
.scui-configuration-button { padding:calc(5px * var(--scui-scaling)) calc(12px * var(--scui-scaling)); }
|
|
671
671
|
.scui-configuration-alert { color:var(--scui-danger,var(--scui-fg)); }
|
|
672
|
+
.scui-configuration-proposed { color:var(--scui-muted,var(--scui-fg)); }
|
|
673
|
+
.scui-configuration-proposed-link { color:var(--scui-accent,currentColor); }
|
|
672
674
|
.scui-configuration-details { margin-top:calc(20px * var(--scui-scaling)); color:var(--scui-muted); }
|
|
673
675
|
.scui-configuration-summary { cursor:pointer; }
|
|
674
676
|
.scui-configuration-input { box-sizing:border-box; min-width:0; max-width:100%; padding:calc(7px * var(--scui-scaling)) calc(9px * var(--scui-scaling)); }
|
|
@@ -704,6 +706,43 @@
|
|
|
704
706
|
.scui-settings-text { margin:0 }
|
|
705
707
|
.scui-settings-footer { display:flex; gap:calc(6px * var(--scui-scaling)); justify-content:flex-end; margin-top:auto; padding:calc(8px * var(--scui-scaling)); border-top:1px solid var(--scui-border); background:var(--scui-bg-raised) }
|
|
706
708
|
.scui-settings-action { padding:calc(6px * var(--scui-scaling)) calc(9px * var(--scui-scaling)); border-radius:var(--scui-button-radius, var(--scui-radius-md)) }
|
|
709
|
+
.scui-setup-jobs { display:grid; gap:6px; margin:0 0 8px; padding:0; list-style:none; }
|
|
710
|
+
.scui-timeline { display:grid; gap:8px; min-width:0; font-size:12.5px; }
|
|
711
|
+
.scui-timeline-header { display:flex; gap:4px; align-items:center; }
|
|
712
|
+
.scui-timeline-button { min-width:28px; padding:2px 8px; }
|
|
713
|
+
.scui-timeline-grid { position:relative; display:grid; gap:2px; min-width:0; }
|
|
714
|
+
.scui-timeline-axis { position:relative; height:18px; margin-left:var(--scui-timeline-label-width,180px); border-bottom:1px solid var(--scui-border); }
|
|
715
|
+
.scui-timeline-tick { position:absolute; transform:translateX(-50%); color:var(--scui-muted); font-size:11px; white-space:nowrap; }
|
|
716
|
+
.scui-timeline-lane { display:grid; grid-template-columns:var(--scui-timeline-label-width,180px) 1fr; align-items:center; min-height:24px; }
|
|
717
|
+
.scui-timeline-lane-label { overflow:hidden; padding-right:8px; color:var(--scui-fg); text-overflow:ellipsis; white-space:nowrap; }
|
|
718
|
+
.scui-timeline-track { position:relative; height:20px; border-radius:4px; background:var(--scui-bg-raised); }
|
|
719
|
+
.scui-timeline-block { position:absolute; top:2px; height:16px; min-width:2px; overflow:hidden; padding:0 4px; border:0; border-radius:3px; background:var(--scui-accent); color:var(--scui-bg); font-size:11px; text-align:left; white-space:nowrap; }
|
|
720
|
+
.scui-timeline-block[data-kind="forecast"] { border:1px dashed var(--scui-accent); background:transparent; color:var(--scui-fg); }
|
|
721
|
+
.scui-timeline-block[data-kind="due"] { width:3px !important; padding:0; background:var(--scui-danger); }
|
|
722
|
+
.scui-timeline-block[data-kind="failed"] { background:var(--scui-danger); }
|
|
723
|
+
.scui-timeline-block[data-kind="cancelled"] { background:var(--scui-muted); }
|
|
724
|
+
.scui-timeline-block[data-kind="waiting"] { background:var(--scui-warning); }
|
|
725
|
+
.scui-timeline-block[data-kind="idle"] { background:var(--scui-fill); color:var(--scui-fg); }
|
|
726
|
+
.scui-timeline-block-label { pointer-events:none; }
|
|
727
|
+
.scui-timeline-overlay { position:absolute; inset:18px 0 0 var(--scui-timeline-label-width,180px); width:calc(100% - var(--scui-timeline-label-width,180px)); height:calc(100% - 18px); overflow:visible; pointer-events:none; }
|
|
728
|
+
.scui-timeline-arrow { fill:none; stroke:var(--scui-muted); stroke-width:1.2; }
|
|
729
|
+
.scui-timeline-now { stroke:var(--scui-danger); stroke-width:1; }
|
|
730
|
+
.scui-timeline-series { display:grid; grid-template-columns:var(--scui-timeline-label-width,180px) 1fr; align-items:center; height:48px; }
|
|
731
|
+
.scui-timeline-series-chart { width:100%; height:44px; }
|
|
732
|
+
.scui-timeline-series-line { fill:none; stroke:var(--scui-accent); stroke-width:1.5; }
|
|
733
|
+
.scui-calendar { display:grid; gap:8px; font-size:12.5px; }
|
|
734
|
+
.scui-calendar-header { display:flex; gap:8px; align-items:center; }
|
|
735
|
+
.scui-calendar-title { min-width:140px; text-align:center; }
|
|
736
|
+
.scui-calendar-grid { display:grid; grid-template-columns:repeat(7,minmax(0,1fr)); gap:2px; margin:0; padding:0; list-style:none; }
|
|
737
|
+
.scui-calendar-day { display:grid; align-content:start; gap:2px; min-height:84px; padding:4px; border-radius:4px; background:var(--scui-bg-raised); }
|
|
738
|
+
.scui-calendar-day[data-outside="true"] { opacity:.45; }
|
|
739
|
+
.scui-calendar-day[data-today="true"] { outline:1px solid var(--scui-accent); }
|
|
740
|
+
.scui-calendar-date { color:var(--scui-muted); font-size:11px; }
|
|
741
|
+
.scui-calendar-entry { overflow:hidden; padding:1px 4px; border:0; border-radius:3px; background:var(--scui-accent); color:var(--scui-bg); font-size:11px; text-align:left; text-overflow:ellipsis; white-space:nowrap; }
|
|
742
|
+
.scui-calendar-entry[data-kind="deadline"] { background:var(--scui-danger); }
|
|
743
|
+
.scui-calendar-entry[data-kind="window"] { background:var(--scui-warning); }
|
|
744
|
+
.scui-calendar-entry[data-kind="job"] { background:var(--scui-fill); color:var(--scui-fg); }
|
|
745
|
+
.scui-calendar-empty { margin:0; color:var(--scui-muted); }
|
|
707
746
|
}
|
|
708
747
|
@layer scui.components {
|
|
709
748
|
/* Team view: sessions, machines, pane access and a pane's terminal over a Teams server. */
|