@volter/supercode-teams 0.2.156 → 0.2.158

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/README.md CHANGED
@@ -47,7 +47,11 @@ a kind another product serves on the same machine. `title.set` needs `control` o
47
47
 
48
48
  A workplace can open one pane or one folder for someone who is not a member of the team. A
49
49
  server started with `--delegated-issuer ORIGIN` (the Worker's `DELEGATED_ISSUER`) trusts short-lived
50
- tokens that issuer signs (Ed25519, keys at `ORIGIN/.well-known/jwks.json`, audience this server's
50
+ tokens that issuer signs. `--delegated-issuers ORIGIN,ORIGIN` (the Worker's
51
+ `DELEGATED_ISSUERS`) adds an explicit list of trusted origins; the legacy singular setting
52
+ remains supported. Keys are cached separately per origin, and each token must match a
53
+ publication or session binding for that exact issuer. Staging never opens a production
54
+ binding or publication. Tokens use Ed25519 (keys at `ORIGIN/.well-known/jwks.json`, audience this server's
51
55
  public origin), each naming a pane (`pane:<key>`) or a folder (`root:/path`) on one machine of one
52
56
  team, and `view` or `control`. A token opens only a surface the machine's custodian **published** on
53
57
  this server (the machine's publications, `/machines/{id}/publications`): the same team, machine and
@@ -10,8 +10,8 @@ const VALUES = new Set([
10
10
  'source', 'cursor', 'limit', 'tail', 'before', 'after', 'revision', 'role',
11
11
  'out', 'ttl', 'type', 'name', 'visibility', 'backfill', 'format', 'to',
12
12
  'grant', 'binding', 'caps', 'fidelity', 'cwd', 'root', 'workspace-from', 'since', 'until', 'session',
13
- 'on', 'timeout', 'plan', 'tmux-socket', 'volter-issuer', 'delegated-issuer', 'workplace', 'organization', 'key', 'room',
14
- 'conversation', 'rh2', 'scope',
13
+ 'on', 'timeout', 'plan', 'tmux-socket', 'volter-issuer', 'delegated-issuer', 'delegated-issuers', 'workplace', 'organization', 'key', 'room',
14
+ 'conversation', 'rh2', 'scope', 'agent',
15
15
  ]);
16
16
 
17
17
  export function parseArgs(argv) {
package/bin/workspace.mjs CHANGED
@@ -120,7 +120,7 @@ async function serverCommand({ flags, sub, rest, home, output }) {
120
120
  // The Node host's store is node:sqlite (Node 22.5+); only this verb loads it, so every client
121
121
  // verb runs on the Node 20 the package declares.
122
122
  const { createTeamsServer } = await import('../server/index.mjs');
123
- const server = createTeamsServer({ dataDir: flags.data_dir ?? resolve(home, 'teams', 'server'), listen: hostPort(flags.listen ?? '127.0.0.1:7331'), publicOrigin: flags.public_origin ?? null, identityIssuer: flags.volter_issuer ?? null, delegatedIssuer: flags.delegated_issuer ?? null, ...(flags.init_team ? { initTeam: flags.init_team } : {}) });
123
+ const server = createTeamsServer({ dataDir: flags.data_dir ?? resolve(home, 'teams', 'server'), listen: hostPort(flags.listen ?? '127.0.0.1:7331'), publicOrigin: flags.public_origin ?? null, identityIssuer: flags.volter_issuer ?? null, delegatedIssuer: flags.delegated_issuer ?? null, delegatedIssuers: flags.delegated_issuers?.split(',').map(origin => origin.trim()) ?? [], ...(flags.init_team ? { initTeam: flags.init_team } : {}) });
124
124
  output(await server.start());
125
125
  await new Promise(resolveStop => { const stop = async () => { await server.close(); resolveStop(); }; process.once('SIGINT', stop); process.once('SIGTERM', stop); });
126
126
  }
@@ -999,7 +999,7 @@ function printHelp() {
999
999
  auth login|status|switch|logout (one Volter sign-in; "supercode login" is "auth login")
1000
1000
  login [ORIGIN] [--team TEAM] [--as CONTEXT] [--token-stdin|--invite-stdin] (Volter sign-in in the browser by default)
1001
1001
  identity link
1002
- server start [--volter-issuer ORIGIN] [--delegated-issuer ORIGIN]
1002
+ server start [--volter-issuer ORIGIN] [--delegated-issuer ORIGIN] [--delegated-issuers ORIGIN,ORIGIN]
1003
1003
  publish pane|root TARGET --room ROOM --workplace ORIGIN --organization ORG [--scope view|control] [--name] [--key] [--machine] [--token-stdin] [--record-only]
1004
1004
  unpublish SURFACE --room ROOM --workplace ORIGIN --organization ORG [--machine] [--token-stdin] [--record-only|--workplace-only]
1005
1005
  connect|disconnect|status
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.156",
3
+ "version": "0.2.158",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",
@@ -406,7 +406,7 @@ async function viewBindingRoute(api,req,res,auth,ref,requestId){
406
406
  if(session.custodian_id!==auth.principal_id)fail(403,'forbidden',"only the session's owner binds it to a view");
407
407
  const body=await parseJson(req);exact(body,['issuer','room','conversation']);
408
408
  if(typeof body.room!=='string'||!body.room||typeof body.conversation!=='string'||!body.conversation)fail(400,'invalid_request','room and conversation are required');
409
- if(!api.delegatedIssuer||body.issuer!==api.delegatedIssuer)fail(400,'invalid_request',`this server accepts views signed by ${api.delegatedIssuer??'no workplace'}`);
409
+ if(!api.delegatedIssuers.includes(body.issuer))fail(400,'invalid_request',`this server accepts views signed by ${api.delegatedIssuers.join(', ')||'no workplace'}`);
410
410
  const key=[auth.team_id,session.id,body.issuer,body.room,body.conversation];
411
411
  await api.store.transaction(req.method==='POST'
412
412
  ?[{sql:'INSERT OR IGNORE INTO session_view_bindings(team_id,session_id,issuer,room,conversation,bound_by,created_at) VALUES (?,?,?,?,?,?,?)',params:[...key,auth.principal_id,api.clock.nowMs()]}]
package/server/index.mjs CHANGED
@@ -16,12 +16,12 @@ import { createTeamsApi, bootstrapServer, maintain, normalizePublicOrigin, produ
16
16
 
17
17
  function atomicJson(path,value){const tmp=`${path}.${process.pid}.${randomBytes(12).toString('hex')}.tmp`;let fd;try{fd=openSync(tmp,'wx',0o600);writeFileSync(fd,JSON.stringify(value,null,2)+'\n');closeSync(fd);fd=undefined;renameSync(tmp,path);chmodSync(path,0o600);}catch(error){if(fd!==undefined)closeSync(fd);try{unlinkSync(tmp);}catch{}throw error;}}
18
18
 
19
- export function createTeamsServer({dataDir,listen={host:'127.0.0.1',port:7331},publicOrigin=null,identityIssuer=null,delegatedIssuer=null,clock=productionClock,ids=productionIds,initTeam,uploadLimitBytes=256*1024*1024}={}) {
19
+ export function createTeamsServer({dataDir,listen={host:'127.0.0.1',port:7331},publicOrigin=null,identityIssuer=null,delegatedIssuer=null,delegatedIssuers=[],clock=productionClock,ids=productionIds,initTeam,uploadLimitBytes=256*1024*1024}={}) {
20
20
  if(!dataDir) throw new TypeError('dataDir is required');
21
21
  const loopback=['127.0.0.1','::1','localhost'].includes(listen.host??'127.0.0.1');
22
22
  publicOrigin=normalizePublicOrigin(publicOrigin);
23
23
  if(!loopback&&!publicOrigin?.startsWith('https:'))throw new TypeError('non-loopback Teams servers require an HTTPS publicOrigin');
24
- const api=createTeamsApi({clock,ids,publicOrigin,identityIssuer,identityOrganizations:identityIssuer?identityOrganizationsFromEnv(process.env):null,delegatedIssuer,uploadLimitBytes});
24
+ const api=createTeamsApi({clock,ids,publicOrigin,identityIssuer,identityOrganizations:identityIssuer?identityOrganizationsFromEnv(process.env):null,delegatedIssuer,delegatedIssuers,uploadLimitBytes});
25
25
  // The harness's own routes on this server (harness-routes.mjs); the handler owns the neutral ones.
26
26
  api.attach({harnessRoutes});
27
27
  const events=api.events;
package/terminal-mail.mjs CHANGED
@@ -14,6 +14,7 @@ function confirmed(batch) {
14
14
  let offset = batch.offset, buffered = '';
15
15
  const decoder = new StringDecoder('utf8');
16
16
  const bytes = Buffer.alloc(64 * 1024);
17
+ let matched = 0, fragments = 0;
17
18
  for (;;) {
18
19
  const count = readSync(fd, bytes, 0, bytes.length, offset);
19
20
  if (!count) return false;
@@ -24,7 +25,22 @@ function confirmed(batch) {
24
25
  let row; try { row = JSON.parse(line); } catch { continue; }
25
26
  const content = batch.kind === 'codex' && row.type === 'response_item' && row.payload?.type === 'message' && row.payload.role === 'user'
26
27
  ? row.payload.content : batch.kind === 'grok' && row.type === 'user' ? row.content : null;
27
- if (textOf(content) === batch.text) return true;
28
+ const text = textOf(content);
29
+ if (text === null || text.length === 0) continue;
30
+ if (text === batch.text) return { fragments: 1 };
31
+ // Older mail delivery omitted the paste delimiter, so terminal newlines
32
+ // submitted one envelope as several native user messages. Recover only
33
+ // exact, ordered fragments of the retained bytes, separated by original
34
+ // newlines. Never infer delivery from an id, substring or empty composer.
35
+ let next = matched;
36
+ while (next < batch.text.length && batch.text[next] === '\n') next++;
37
+ if (matched && batch.text.startsWith(text, next)) {
38
+ matched = next + text.length; fragments++;
39
+ } else if (batch.text.startsWith(text)) {
40
+ matched = text.length; fragments = 1;
41
+ } else { matched = 0; fragments = 0; }
42
+ if (matched === batch.text.length) return { fragments };
43
+
28
44
  }
29
45
  }
30
46
  } finally { closeSync(fd); }
@@ -50,7 +66,7 @@ export class TerminalMailbox {
50
66
  const delivered = ids.every(id => this.state.receipts.some(r => r.message_ids.includes(id)));
51
67
  const batch = this.state.active;
52
68
  return { delivered, queued: !delivered, message_ids: ids,
53
- ...(delivered ? { receipt: 'native-user-message' } : { reason: batch?.error ?? 'queued', batch: batch?.id ?? null }) };
69
+ ...(delivered ? { receipt: this.state.receipts.some(r => r.receipt === 'native-user-messages' && r.message_ids.some(id => ids.includes(id))) ? 'native-user-messages' : 'native-user-message' } : { reason: batch?.error ?? 'queued', batch: batch?.id ?? null }) };
54
70
  }
55
71
  enqueue(text, ids, identity, host, session) {
56
72
  const messageIds = ids?.length ? ids : [`input-${randomUUID()}`];
@@ -95,7 +111,7 @@ export class TerminalMailbox {
95
111
  if (!batch) return;
96
112
  const seen = batch.offset !== null ? confirmed(batch) : (await this.load(batch.locator, { message_offset: batch.messageOffset, message_limit: 1000 })).session?.messages?.some(message => message.role === 'user' && textOf(message.content) === batch.text);
97
113
  if (seen) {
98
- const receipt = { batch: batch.id, message_ids: batch.message_ids, delivered: true, receipt: 'native-user-message', at: Date.now() };
114
+ const receipt = { batch: batch.id, message_ids: batch.message_ids, delivered: true, receipt: seen.fragments > 1 ? 'native-user-messages' : 'native-user-message', fragments: seen.fragments ?? 1, at: Date.now() };
99
115
  appendFileSync(join(this.dir, 'receipts.jsonl'), JSON.stringify(receipt) + '\n', { mode: 0o600 });
100
116
  this.state.receipts.push(receipt);
101
117
  this.state.active = null; this.save(); this.log(receipt); return;
@@ -104,7 +120,9 @@ export class TerminalMailbox {
104
120
  // Persist uncertainty before I/O. A daemon restart checks the transcript;
105
121
  // it never guesses that an unacknowledged paste did not happen.
106
122
  batch.phase = 'paste_started'; this.save();
107
- await this.host.sendInput(this.session.id, batch.text, { submitMode: 'none' });
123
+ // The terminal host's final newline selects one bracketed paste;
124
+ // submitMode:none holds the separate Enter until the paste completes.
125
+ await this.host.sendInput(this.session.id, `${batch.text}\n`, { submitMode: 'none' });
108
126
  batch.phase = 'pasted'; this.save();
109
127
  }
110
128
  if (batch.phase === 'paste_started') {
package/worker/index.mjs CHANGED
@@ -81,7 +81,7 @@ export class TeamsServer extends DurableObject {
81
81
  console.log(`storage: database_bytes=${ctx.storage.sql.databaseSize}`);
82
82
  const db = durableSql(ctx.storage);
83
83
  migrate({ exec: db.exec, tx: db.tx, version: () => ctx.storage.kv.get('schema_version') ?? 0, setVersion: (n) => ctx.storage.kv.put('schema_version', n) });
84
- this.api = createTeamsApi({ publicOrigin: normalizePublicOrigin(env.PUBLIC_ORIGIN), identityIssuer: env.VOLTER_ISSUER || null, identityOrganizations: identityOrganizationsFromEnv(env), delegatedIssuer: env.DELEGATED_ISSUER || null });
84
+ this.api = createTeamsApi({ publicOrigin: normalizePublicOrigin(env.PUBLIC_ORIGIN), identityIssuer: env.VOLTER_ISSUER || null, identityOrganizations: identityOrganizationsFromEnv(env), delegatedIssuer: env.DELEGATED_ISSUER || null, delegatedIssuers: env.DELEGATED_ISSUERS ? env.DELEGATED_ISSUERS.split(',').map(origin => origin.trim()) : [] });
85
85
  this.api.storageUsage = () => {
86
86
  const sql = ctx.storage.sql;
87
87
  let freeBytes = null;
@@ -4,20 +4,24 @@
4
4
  # --var PUBLIC_ORIGIN:https://<worker>.<account>.workers.dev --var INIT_TEAM:<first team>
5
5
  # The token printed above is the owner's first credential (`supercode teams login <origin> --token-stdin`).
6
6
  #
7
- # A deployment's variables live with the deployment (the first deploy's --var, or the dashboard), not here:
8
- # keep_vars leaves them in place, so a later deploy of new code never resets its origin or issuers.
7
+ # Origin and identity variables are retained with keep_vars. The explicit delegated
8
+ # allowlist below is versioned and installed by deploy-teams CI.
9
9
  # PUBLIC_ORIGIN the https origin the Worker is served at (required)
10
10
  # INIT_TEAM the first team's name, used once at bootstrap
11
11
  # VOLTER_ISSUER optional: members sign in with Volter (supercode teams login --volter); the issuer
12
12
  # must have this origin registered as a resource
13
13
  # DELEGATED_ISSUER optional: a workplace (RH2) whose delegated tokens open published panes and folders and
14
14
  # bound sessions; its keys are at <issuer>/.well-known/jwks.json
15
+ # DELEGATED_ISSUERS comma-separated exact origins, additive with legacy DELEGATED_ISSUER
15
16
  name = "supercode-teams"
16
17
  main = "index.mjs"
17
18
  compatibility_date = "2026-09-01"
18
19
  compatibility_flags = ["nodejs_compat"]
19
20
  keep_vars = true
20
21
 
22
+ [vars]
23
+ DELEGATED_ISSUERS = "https://pilot.runhuman.com,https://rh2-staging.voltertest.xyz"
24
+
21
25
  # Workers Logs: the Worker's log lines (viewer-error, session-view refused, maintain) kept and searchable after the fact.
22
26
  [observability]
23
27
  enabled = true