@volter/teams 0.1.24 → 0.1.25
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/package.json +1 -1
- package/server/http.mjs +3 -1
- package/server/machine-link.mjs +23 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/teams",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
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/http.mjs
CHANGED
|
@@ -36,7 +36,9 @@ export function createHttpHandler(api){return async(req,res)=>{const requestId=a
|
|
|
36
36
|
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);}
|
|
37
37
|
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);}
|
|
38
38
|
if(!auth)fail(401,'authentication_required','authentication required');auth.now=api.clock.nowMs();auth.clock=api.clock;
|
|
39
|
-
const bulk=/\/captures\/[^/]+\/parts\/\d+$/.test(path)||/\/uploads\/[^/]+\/chunks\/\d+$/.test(path)
|
|
39
|
+
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
|
|
40
|
+
// gets the bulk allowance for ordinary requests too; a person's or a service's stays at 120 a minute.
|
|
41
|
+
checkRate(api,`credential:${auth.credential_id}:${bulk?'bulk':'ordinary'}`,bulk||auth.kind==='machine'?600:120,bulk||auth.kind==='machine'?60:30);
|
|
40
42
|
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);}
|
|
41
43
|
if(req.method==='POST'&&path==='/api/v1/me/identities')return await linkIdentity(api,req,res,auth,requestId);
|
|
42
44
|
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',...(api.identityIssuer?['volter']:[])],...(api.identityIssuer?{identity:{issuer:api.identityIssuer,audience:api.publicOrigin}}:{})},requestId);
|
package/server/machine-link.mjs
CHANGED
|
@@ -305,6 +305,17 @@ async function acceptLink(api, teamId, machineId, duplex, closeSocket) {
|
|
|
305
305
|
});
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
const LINK_WAIT_MS = 10_000;
|
|
309
|
+
|
|
310
|
+
/** The machine's live link, waiting up to `ms` for one that is relinking. */
|
|
311
|
+
async function linkWithin(api, linkKey, ms) {
|
|
312
|
+
for (const deadline = Date.now() + ms; ; await new Promise((resolve) => setTimeout(resolve, 250))) {
|
|
313
|
+
const link = links(api).get(linkKey);
|
|
314
|
+
if (link && !link.channel.closed) return link;
|
|
315
|
+
if (Date.now() >= deadline) return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
308
319
|
/** A caller's channel: each stream it opens is forwarded to the machine with a fresh claim. */
|
|
309
320
|
async function acceptChannel(api, auth, machineId, duplex, closeSocket) {
|
|
310
321
|
let channel;
|
|
@@ -326,13 +337,19 @@ async function acceptChannel(api, auth, machineId, duplex, closeSocket) {
|
|
|
326
337
|
const claim = await claimFor(api, auth, machineId).catch(() => null);
|
|
327
338
|
if (!claim) return refuse('no machine grant for this caller');
|
|
328
339
|
entry.claim = claim;
|
|
329
|
-
|
|
330
|
-
|
|
340
|
+
// A machine relinks within seconds of this server restarting (a deploy drops every link): a stream waits for its
|
|
341
|
+
// link that long, and retries once on a link that closed under it, rather than refusing a message mid-relink.
|
|
331
342
|
let upstream;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
343
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
344
|
+
const link = await linkWithin(api, key(auth.team_id, machineId), LINK_WAIT_MS);
|
|
345
|
+
if (!link) return refuse('machine_offline: the machine is not linked to this server');
|
|
346
|
+
try {
|
|
347
|
+
upstream = await link.channel.open(stream.name, { ...(stream.params ?? {}), _admitted: claim });
|
|
348
|
+
break;
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (attempt === 0 && link.channel.closed) continue;
|
|
351
|
+
return refuse(error.message.replace(/^[a-z]+ refused: /, ''));
|
|
352
|
+
}
|
|
336
353
|
}
|
|
337
354
|
accept();
|
|
338
355
|
const pair = { close: (reason) => { if (!stream.closed) stream.close(reason); if (!upstream.closed) upstream.close(reason); } };
|