@volter/teams 0.1.8 → 0.1.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/teams",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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",
@@ -38,7 +38,7 @@
38
38
  "node": ">=22"
39
39
  },
40
40
  "dependencies": {
41
- "@volter/identity": "^0.4.0",
41
+ "@volter/identity": "^0.4.1",
42
42
  "ws": "^8.21.0"
43
43
  }
44
44
  }
@@ -50,6 +50,39 @@ async function issuerKey(api, kid) {
50
50
  * target is null, and the caller refuses it exactly as an unknown credential.
51
51
  */
52
52
  export async function delegatedAccessOf(api, token) {
53
+ const claims = await verifiedClaims(api, token);
54
+ if (!claims) return null;
55
+ const surface = claims.surface;
56
+ const caps = CAPS[surface?.kind]?.[claims.scope];
57
+ const config = surface?.config ?? {};
58
+ if (!caps || typeof config.team !== 'string' || typeof config.machine !== 'string' || typeof config.target !== 'string') return null;
59
+ const target = surface.kind === 'pane' ? (config.target.startsWith('pane:') ? config.target : null) : (config.target.startsWith('root:/') ? config.target : null);
60
+ if (!target) return null;
61
+ return { caps, expiresAt: claims.exp * 1000, issuer: claims.iss, kind: surface.kind, machineId: config.machine, scope: claims.scope, subject: claims.sub, target, teamId: config.team };
62
+ }
63
+
64
+ /** Longest a session-view token may live, whatever it says: the workplace mints them for minutes. */
65
+ const SESSION_TOKEN_MAX_S = 15 * 60;
66
+
67
+ /**
68
+ * What a session-view token admits (RH2 ADR 0008), or null: { subject, issuer, teamId, sessionRef, room, conversation,
69
+ * scope, expiresAt }. It names one session (by its id or the harness's own) of one team, and the Room and Conversation
70
+ * viewing it; the server admits it only for a session its owner bound to that Room and Conversation.
71
+ */
72
+ export async function delegatedSessionOf(api, token) {
73
+ const claims = await verifiedClaims(api, token);
74
+ if (!claims) return null;
75
+ const session = claims.session;
76
+ if (!session || typeof session.team !== 'string' || typeof session.id !== 'string' || !session.id) return null;
77
+ if (typeof claims.room !== 'string' || !claims.room || typeof claims.conversation !== 'string' || !claims.conversation) return null;
78
+ if (!['view', 'control'].includes(claims.scope)) return null;
79
+ const now = Math.floor(api.clock.nowMs() / 1000);
80
+ if (claims.exp - now > SESSION_TOKEN_MAX_S || (typeof claims.iat === 'number' && claims.exp - claims.iat > SESSION_TOKEN_MAX_S)) return null;
81
+ return { conversation: claims.conversation, expiresAt: claims.exp * 1000, issuer: claims.iss, room: claims.room, scope: claims.scope, sessionRef: session.id, subject: claims.sub, teamId: session.team };
82
+ }
83
+
84
+ /** A token's claims when it is one this server trusts: signed by the issuer's key, for this server, in its lifetime. */
85
+ async function verifiedClaims(api, token) {
53
86
  if (!api.delegatedIssuer || !api.publicOrigin || !looksDelegated(token)) return null;
54
87
  try {
55
88
  const [head, body, signature] = token.split('.');
@@ -64,13 +97,7 @@ export async function delegatedAccessOf(api, token) {
64
97
  if (claims.iss !== api.delegatedIssuer || claims.aud !== api.publicOrigin) return null;
65
98
  if (typeof claims.exp !== 'number' || claims.exp <= now - SKEW_S || (typeof claims.nbf === 'number' && claims.nbf > now + SKEW_S)) return null;
66
99
  if (typeof claims.sub !== 'string' || !claims.sub) return null;
67
- const surface = claims.surface;
68
- const caps = CAPS[surface?.kind]?.[claims.scope];
69
- const config = surface?.config ?? {};
70
- if (!caps || typeof config.team !== 'string' || typeof config.machine !== 'string' || typeof config.target !== 'string') return null;
71
- const target = surface.kind === 'pane' ? (config.target.startsWith('pane:') ? config.target : null) : (config.target.startsWith('root:/') ? config.target : null);
72
- if (!target) return null;
73
- return { caps, expiresAt: claims.exp * 1000, issuer: claims.iss, kind: surface.kind, machineId: config.machine, scope: claims.scope, subject: claims.sub, target, teamId: config.team };
100
+ return claims;
74
101
  } catch {
75
102
  return null;
76
103
  }
@@ -0,0 +1,11 @@
1
+ // Schema migration 005 (SQLite DDL), applied in order by store-core migrate().
2
+ export default `-- Schema 5: a session's owner lets one workplace Room's Conversation view it (a session
3
+ -- view, RH2 ADR 0008). A token that workplace signs for that Room and Conversation reads
4
+ -- the session; nothing else reaches it through the token.
5
+ CREATE TABLE session_view_bindings (
6
+ team_id TEXT NOT NULL, session_id TEXT NOT NULL, issuer TEXT NOT NULL, room TEXT NOT NULL,
7
+ conversation TEXT NOT NULL, bound_by TEXT NOT NULL, created_at INTEGER NOT NULL,
8
+ PRIMARY KEY(team_id, session_id, issuer, room, conversation),
9
+ FOREIGN KEY(team_id, session_id) REFERENCES sessions(team_id, id)
10
+ ) STRICT;
11
+ `;
@@ -3,4 +3,5 @@ import m001 from './001.mjs';
3
3
  import m002 from './002.mjs';
4
4
  import m003 from './003.mjs';
5
5
  import m004 from './004.mjs';
6
- export const MIGRATIONS = [m001, m002, m003, m004];
6
+ import m005 from './005.mjs';
7
+ export const MIGRATIONS = [m001, m002, m003, m004, m005];