@volter/teams 0.1.84 → 0.1.85

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.
@@ -59,7 +59,7 @@ export declare class TurnDeliveryUnconfirmedError extends Error {
59
59
  * does not undo delivery. After acceptance, a lost, invalid or cancelled receipt
60
60
  * rejects with TurnDeliveryUnconfirmedError. Reuse the same key when retrying it.
61
61
  */
62
- export function sendBrowserTurn(channel: BrowserChannel, text: string, options?: { turnKey?: string; signal?: AbortSignal }): Promise<BrowserTurnAnswer>;
62
+ export function sendBrowserTurn(channel: BrowserChannel, text: string, options?: { turnKey?: string; target?: { harness: string; session: string }; signal?: AbortSignal }): Promise<BrowserTurnAnswer>;
63
63
 
64
64
  export interface MailHistoryEntry {
65
65
  id: string; from: string; fromName: string; to: string; text: string; at: number;
@@ -178,12 +178,15 @@ export class TurnDeliveryUnconfirmedError extends Error {
178
178
  * is free), or `{ code, text }` when it was not delivered; rejects when the machine refuses the stream.
179
179
  * `turnKey`: one key per line the person typed, reused when they retry it, so a line the machine took after its
180
180
  * first send was given up on is not typed twice (a machine of door vocabulary 3 or later keeps each key once).
181
+ * `target`: on a machine channel, the session that takes the line (`{ harness, session }`, the session's native
182
+ * id); a session channel's grant names its session, and needs none.
181
183
  * Receipt waits are bounded to thirty seconds and 256 KB. `signal` cancels the
182
184
  * wait; neither cancellation nor timeout undoes a turn already accepted.
183
185
  */
184
- export async function sendBrowserTurn(channel, text, { turnKey, signal } = {}) {
186
+ export async function sendBrowserTurn(channel, text, { turnKey, target, signal } = {}) {
185
187
  signal?.throwIfAborted();
186
- const stream = await channel.open('mail', { request: { op: 'turn', text, ...(turnKey ? { turn_key: String(turnKey) } : {}) } });
188
+ const named = target ? { harness: String(target.harness), session: String(target.session) } : {};
189
+ const stream = await channel.open('mail', { request: { op: 'turn', text, ...named, ...(turnKey ? { turn_key: String(turnKey) } : {}) } });
187
190
  return new Promise((resolve, reject) => {
188
191
  let answer = '', bytes = 0, settled = false;
189
192
  const cleanup = () => { clearTimeout(timer); stream.off('data', data); stream.off('close', closed); signal?.removeEventListener('abort', cancelled); };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/teams",
3
- "version": "0.1.84",
3
+ "version": "0.1.85",
4
4
  "type": "module",
5
5
  "description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
6
6
  "license": "MIT",
package/server/apps.mjs CHANGED
@@ -153,6 +153,12 @@ export async function installedIssuer(api, issuer, action, teamId = null) {
153
153
  WHERE a.issuer=? AND a.state='active' AND i.state='active' AND m.active=1 AND installer.active=1 AND installer.role IN ('owner','admin') AND (? IS NULL OR i.team_id=?) AND EXISTS (SELECT 1 FROM json_each(i.actions_json) WHERE value=?) LIMIT 1`, [issuer, teamId, teamId, action]);
154
154
  }
155
155
 
156
+ /** Every issuer some team trusts for sign-in (installedIssuer's trust, any team): what a client may offer. */
157
+ export async function identityIssuers(api) {
158
+ return (await api.store.query(`SELECT DISTINCT a.issuer FROM app_installations i JOIN apps a ON a.id=i.app_id JOIN memberships m ON m.team_id=i.team_id AND m.principal_id=i.principal_id JOIN memberships installer ON installer.team_id=i.team_id AND installer.principal_id=i.installer_id
159
+ WHERE a.issuer IS NOT NULL AND a.state='active' AND i.state='active' AND m.active=1 AND installer.active=1 AND installer.role IN ('owner','admin') AND EXISTS (SELECT 1 FROM json_each(i.actions_json) WHERE value='identity.login') ORDER BY a.issuer`, [])).map(row => row.issuer);
160
+ }
161
+
156
162
  /** Fan-out is durable and transactionally committed with the source mutation. C2 adds resource events. */
157
163
  export async function appEventStatements(api, teamId, kind, data, addedDestination = null) {
158
164
  const now = api.clock.nowMs(), eventId = api.ids.next('event');
package/server/http.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { appRoute, appPreAuth, appAuthentication, enforceAppScope, appAuthGuards, installedIssuer } from './apps.mjs';
1
+ import { appRoute, appPreAuth, appAuthentication, enforceAppScope, appAuthGuards, installedIssuer, identityIssuers } from './apps.mjs';
2
2
  import { machineEventStream } from './machine-events.mjs';
3
3
  import { randomBytes } from 'node:crypto';
4
4
  import { bearer, hashToken, canManage, isAdmin, makeToken } from './auth.mjs';
@@ -40,13 +40,16 @@ export function createHttpHandler(api){return async(req,res)=>{const requestId=a
40
40
  if(req.method==='POST'&&path==='/api/v1/invitations/accept'){checkRate(api,`login:${req.socket.remoteAddress}`,10,5);const body=await parseJson(req);return await acceptInvite(api,res,body,requestId);}
41
41
  if(req.method==='POST'&&path==='/api/v1/login/volter'){checkRate(api,`login:${req.socket.remoteAddress}`,10,5);const body=await parseJson(req);return await identityLogin(api,res,body,requestId);}
42
42
  if(req.method==='POST'&&path==='/api/v1/login/volter/teams'){checkRate(api,`login-teams:${req.socket.remoteAddress}`,10,5);const body=await parseJson(req);return await identityTeams(api,res,body,requestId);}
43
+ // How to sign in is public, as a client needs it before it has a credential: the modes this server takes and, for
44
+ // identity, the issuers some team trusts and the doors that exchange an issuer's token (identity.mjs verifies any
45
+ // OIDC issuer), so a client follows what is advertised rather than naming a provider.
46
+ if(req.method==='GET'&&path==='/api/v1/capabilities'){checkRate(api,`capabilities:${req.socket.remoteAddress}`,60,30);const issuers=api.publicOrigin?await identityIssuers(api):[];return send(res,200,{version:1,server_id:api.serverId,protocol:'teams.v1',operations:['catalog','search','read','follow','access','ingest'],auth_modes:['token','invitation','installed-app',...(issuers.length?['identity']:[])],app_token_exchange:'/api/v1/apps/token',...(issuers.length?{identity:{issuers,audience:api.publicOrigin,login:'/api/v1/login/volter',teams:'/api/v1/login/volter/teams'}}:{})},requestId);}
43
47
  if(!auth)fail(401,'authentication_required','authentication required');auth.now=api.clock.nowMs();auth.clock=api.clock;
44
48
  const bulk=/\/captures\/[^/]+\/parts\/\d+$/.test(path)||/\/uploads\/[^/]+\/chunks\/\d+$/.test(path);// A machine's own credential syncs its live sessions as they change (a capture is a begin and a commit), so it
45
49
  // gets the bulk allowance for ordinary requests too; a person's or a service's stays at 120 a minute.
46
50
  checkRate(api,`credential:${auth.credential_id}:${bulk?'bulk':'ordinary'}`,bulk||auth.kind==='machine'?600:120,bulk||auth.kind==='machine'?60:30);
47
51
  if(req.method==='GET'&&path==='/api/v1/me'){if(!['user','service'].includes(auth.kind))fail(403,'forbidden','credential kind cannot use this operation');const teams=await api.store.query('SELECT t.id,t.name,t.revision,m.role FROM memberships m JOIN teams t ON t.id=m.team_id WHERE m.principal_id=? AND m.active=1 AND (? IS NULL OR m.team_id=?)',[auth.principal_id,auth.team_id,auth.team_id]);return send(res,200,{server_id:api.serverId,principal:{id:auth.principal_id,kind:auth.principal_kind,display_name:auth.display_name},teams:teams.map(x=>({team:{id:x.id,name:x.name,revision:x.revision},role:x.role}))},requestId);}
48
52
  if(req.method==='POST'&&path==='/api/v1/me/identities')return await linkIdentity(api,req,res,auth,requestId);
49
- if(req.method==='GET'&&path==='/api/v1/capabilities')return send(res,200,{version:1,server_id:api.serverId,protocol:'teams.v1',operations:['catalog','search','read','follow','access','ingest'],auth_modes:['token','invitation','installed-app'],app_token_exchange:'/api/v1/apps/token'},requestId);
50
53
  if(req.method==='GET'&&path==='/api/v1/teams')return await listTeams(api,res,auth,url,requestId);
51
54
  if(req.method==='POST'&&path==='/api/v1/teams')return await createTeam(api,req,res,auth,path,requestId);
52
55
  const match=/^\/api\/v1\/teams\/([^/]+)(\/.*)?$/.exec(path);if(!match)fail(404,'not_found','not found');const teamId=match[1],sub=match[2]??'';