@volter/supercode-teams 0.2.155 → 0.2.157

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.155",
3
+ "version": "0.2.157",
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",
@@ -359,14 +359,17 @@ async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){cons
359
359
  carriedRevision = session.revision;
360
360
  const hash = createHash('sha256').update('['), sent = new Set(messages.map(m => m.native_key));
361
361
  while (carriedCount < count) {
362
- const row = await api.store.one(`SELECT ordinal,source_id,native_key,recorded_at,value_json FROM messages m
363
- WHERE m.team_id=? AND m.session_id=? AND ${MESSAGE_AT()} AND m.ordinal=?`,
364
- [auth.team_id, origin.session_id, carriedRevision, carriedRevision, carriedCount]);
365
- if (!row || row.source_id !== sourceId) fail(409, 'carry_mismatch', 'the carried messages are not what this session holds; send the capture whole');
366
- if (sent.has(row.native_key)) fail(409, 'capture_conflict', 'duplicate native message key');
367
- if (carriedCount) hash.update(',');
368
- hash.update(canonicalJson({native_key:row.native_key, recorded_at:row.recorded_at ?? null, value:JSON.parse(row.value_json)}));
369
- carriedCount++;
362
+ const page = await api.store.query(`SELECT ordinal,source_id,native_key,recorded_at,value_json FROM messages m
363
+ WHERE m.team_id=? AND m.session_id=? AND m.ordinal>=? AND m.ordinal<? AND ${MESSAGE_AT()} ORDER BY m.ordinal LIMIT 16`,
364
+ [auth.team_id, origin.session_id, carriedCount, count, carriedRevision, carriedRevision]);
365
+ if (!page.length) fail(409, 'carry_mismatch', 'the carried messages are not what this session holds; send the capture whole');
366
+ for (const row of page) {
367
+ if (row.ordinal !== carriedCount || row.source_id !== sourceId) fail(409, 'carry_mismatch', 'the carried messages are not what this session holds; send the capture whole');
368
+ if (sent.has(row.native_key)) fail(409, 'capture_conflict', 'duplicate native message key');
369
+ if (carriedCount) hash.update(',');
370
+ hash.update(canonicalJson({native_key:row.native_key, recorded_at:row.recorded_at ?? null, value:JSON.parse(row.value_json)}));
371
+ carriedCount++;
372
+ }
370
373
  }
371
374
  if (hash.update(']').digest('hex') !== digest) fail(409, 'carry_mismatch', 'the carried messages are not what this session holds; send the capture whole');
372
375
  }
@@ -403,7 +406,7 @@ async function viewBindingRoute(api,req,res,auth,ref,requestId){
403
406
  if(session.custodian_id!==auth.principal_id)fail(403,'forbidden',"only the session's owner binds it to a view");
404
407
  const body=await parseJson(req);exact(body,['issuer','room','conversation']);
405
408
  if(typeof body.room!=='string'||!body.room||typeof body.conversation!=='string'||!body.conversation)fail(400,'invalid_request','room and conversation are required');
406
- 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'}`);
407
410
  const key=[auth.team_id,session.id,body.issuer,body.room,body.conversation];
408
411
  await api.store.transaction(req.method==='POST'
409
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/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