@volter/supercode-teams 0.2.28 → 0.2.29
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 +22 -9
- package/bin/workspace/args.mjs +1 -1
- package/bin/workspace.mjs +73 -34
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -49,15 +49,28 @@ A workplace can open one pane or one folder for someone who is not a member of t
|
|
|
49
49
|
server started with `--delegated-issuer ORIGIN` (the Worker's `DELEGATED_ISSUER`) trusts short-lived
|
|
50
50
|
tokens that issuer signs (Ed25519, keys at `ORIGIN/.well-known/jwks.json`, audience this server's
|
|
51
51
|
public origin), each naming a pane (`pane:<key>`) or a folder (`root:/path`) on one machine of one
|
|
52
|
-
team, and `view` or `control`.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
team, and `view` or `control`. A token opens only a surface the machine's custodian **published** on
|
|
53
|
+
this server (the machine's publications, `/machines/{id}/publications`): the same team, machine and
|
|
54
|
+
exact target, published to that workplace and to the Room the token names, by the machine's current
|
|
55
|
+
custodian while still an active member of the team. A token naming no Room, or one for
|
|
56
|
+
anything else, for a wildcard (`pane:*`) or for the whole filesystem (`root:/`) opens nothing, and a
|
|
57
|
+
token must carry `iat` and live at most 15 minutes (`exp - iat` and `exp - now`). The hosted viewer
|
|
58
|
+
at `/view#token=…` opens it; the server turns the token into a machine grant on exactly that target
|
|
59
|
+
(a pane: `observe`, and `control` to type; a folder: `observe` to read, `files` to change) that
|
|
60
|
+
expires with the token, and closes the channel then. A page embedding the viewer sends it a fresh
|
|
61
|
+
token by `postMessage({ type: 'delegated-token', token })`, which the viewer asks for with
|
|
62
|
+
`delegated-token-needed`.
|
|
63
|
+
|
|
64
|
+
`teams publish` records the surface on this server (only the machine's custodian can), then puts
|
|
65
|
+
the pane or folder of the connected machine (or `--machine NAME`) into a workplace Room as a
|
|
66
|
+
Surface, through the workplace's own door with a session token of yours; if the workplace refuses,
|
|
67
|
+
the record is withdrawn. `teams unpublish SURFACE` (the surface key or the publication id)
|
|
68
|
+
deletes this server's record first, on whichever of your machines holds it, which deletes the
|
|
69
|
+
grants minted on it and closes their channels at once, then retires it in the workplace. It fails
|
|
70
|
+
when this server holds no such record, unless `--workplace-only`; `--record-only` deletes the
|
|
71
|
+
record here without a workplace call or token. Removing a member likewise ends every delegated
|
|
72
|
+
grant on the surfaces they published. A surface published before this server kept records opens nothing until its custodian
|
|
73
|
+
records it with `teams publish … --record-only` (no workplace call, no workplace token):
|
|
61
74
|
|
|
62
75
|
```sh
|
|
63
76
|
supercode teams publish pane <ctx> --room room_… --workplace https://pilot.runhuman.com --organization acme --token-stdin
|
package/bin/workspace/args.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const BOOLEANS = new Set([
|
|
2
2
|
'json', 'help', 'live', 'saved', 'watch', 'paginate', 'token-stdin',
|
|
3
3
|
'invite-stdin', 'include-children', 'rotate', 'install', 'force',
|
|
4
|
-
'take-control', 'dry-run', 'volter', 'foreground', 'no-connect',
|
|
4
|
+
'take-control', 'dry-run', 'volter', 'foreground', 'no-connect', 'record-only', 'workplace-only',
|
|
5
5
|
]);
|
|
6
6
|
|
|
7
7
|
const VALUES = new Set([
|
package/bin/workspace.mjs
CHANGED
|
@@ -575,8 +575,10 @@ async function machinesCommand(sub, { flags, rest, output, client, context, team
|
|
|
575
575
|
const machine = reference.startsWith('mach_') ? reference : await resolveName((await team.machines.list()).items, reference, item => item.name, item => item.id, 'machine');
|
|
576
576
|
const machineBase = teamPath(context, `/machines/${machine}/access`), liveBase = teamPath(context, `/machines/${machine}/grants`);
|
|
577
577
|
if (action === 'list') {
|
|
578
|
-
|
|
579
|
-
|
|
578
|
+
// A team owner or admin manages a machine's grants; its live session grants are its custodian's alone, so
|
|
579
|
+
// for anyone else that half is not found and the machine grants still answer.
|
|
580
|
+
const [machineGrants, liveGrants] = await Promise.all([client.request('GET', machineBase), client.request('GET', liveBase).catch(error => { if (error.code === 'not_found') return null; throw error; })]);
|
|
581
|
+
return output({ version: 1, context: scope(context), data: { machine_grants: machineGrants.items, live_grants: liveGrants?.items ?? null } });
|
|
580
582
|
}
|
|
581
583
|
if (action === 'revoke') {
|
|
582
584
|
if (!flags.grant) usage('--grant is required');
|
|
@@ -745,40 +747,77 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
745
747
|
/**
|
|
746
748
|
* PUBLISH: one pane or folder of a machine of this team, put into a workplace Room as a Surface (RH2's Surface
|
|
747
749
|
* door). The workplace decides who in the Room may see or drive it and signs them delegated tokens; this server
|
|
748
|
-
* admits those if it was started with that workplace as its delegated issuer (server/delegated.mjs)
|
|
749
|
-
*
|
|
750
|
-
*
|
|
750
|
+
* admits those if it was started with that workplace as its delegated issuer (server/delegated.mjs), and only for a
|
|
751
|
+
* surface its custodian published: so `publish` first records the surface on this server (the machine's
|
|
752
|
+
* publications, server/publications.mjs), then puts it into the workplace, and withdraws the record if the workplace
|
|
753
|
+
* refuses. `--record-only` records it here without touching the workplace (a surface put there before this server kept
|
|
754
|
+
* records). The machine is the one `teams connect` enrolled here, or `--machine NAME`. The workplace credential is a
|
|
755
|
+
* session token, from --token-stdin or WORKPLACE_TOKEN. `unpublish SURFACE` (the surface key, or the publication id)
|
|
756
|
+
* deletes this server's record first, on whichever of the caller's machines holds it (every token for it stops working
|
|
757
|
+
* at once), then retires it in the workplace; it fails when this server holds no such record, unless
|
|
758
|
+
* `--workplace-only`. `unpublish --record-only` deletes the record here and leaves the workplace alone.
|
|
751
759
|
*/
|
|
752
|
-
async function publishCommand(verb, sub, { flags, rest, output, context, team, home, env }) {
|
|
753
|
-
only(flags, ['json', 'help', 'context', 'room', 'workplace', 'organization', 'name', 'key', 'machine', 'token_stdin']);
|
|
754
|
-
|
|
760
|
+
async function publishCommand(verb, sub, { flags, rest, output, context, team, client, home, env }) {
|
|
761
|
+
only(flags, ['json', 'help', 'context', 'room', 'workplace', 'organization', 'name', 'key', 'machine', 'token_stdin', 'record_only', 'workplace_only']);
|
|
762
|
+
const recordOnly = Boolean(flags.record_only);
|
|
763
|
+
if (!flags.room || !flags.workplace || (!recordOnly && !flags.organization)) usage(`teams ${verb} needs --room ROOM --workplace ORIGIN --organization ORG`);
|
|
755
764
|
const workplace = new URL(flags.workplace).origin;
|
|
756
|
-
const token = flags.token_stdin ? readFileSync(0, 'utf8').trim() : env.WORKPLACE_TOKEN;
|
|
757
|
-
if (!token) usage('the workplace credential comes from --token-stdin or WORKPLACE_TOKEN');
|
|
765
|
+
const token = recordOnly ? null : flags.token_stdin ? readFileSync(0, 'utf8').trim() : env.WORKPLACE_TOKEN;
|
|
766
|
+
if (!recordOnly && !token) usage('the workplace credential comes from --token-stdin or WORKPLACE_TOKEN');
|
|
767
|
+
const machine = flags.machine
|
|
768
|
+
? resolveName((await team.machines.list()).items ?? [], flags.machine, item => item.name, item => item.id, 'machine')
|
|
769
|
+
: new WorkspaceContexts(home).connector(context.server_id, context.team_id).read()?.machine?.id;
|
|
770
|
+
if (!machine && verb === 'publish') usage('this machine is not connected to the team: run teams connect, or name --machine');
|
|
771
|
+
const publicationsOf = (id) => teamPath(context, `/machines/${encodeURIComponent(id)}/publications`);
|
|
772
|
+
const publications = machine ? publicationsOf(machine) : null;
|
|
773
|
+
const recorded = async () => (await client.request('GET', publications)).items ?? [];
|
|
758
774
|
const roomPath = `${workplace}/api/v3/rooms/${encodeURIComponent(flags.room)}/surfaces`;
|
|
759
|
-
|
|
775
|
+
const workplaceCall = async (url, body) => {
|
|
776
|
+
const response = await fetch(url, { body: JSON.stringify(body), headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json', origin: workplace, 'x-rh2-organization': String(flags.organization) }, method: 'POST' });
|
|
777
|
+
const payload = await response.json().catch(() => null);
|
|
778
|
+
if (!response.ok) throw Object.assign(new Error(payload?.error?.message ?? `the workplace answered ${response.status}`), { code: response.status === 403 || response.status === 404 ? 'forbidden' : response.status === 401 ? 'authentication_required' : 'invalid_request' });
|
|
779
|
+
return payload;
|
|
780
|
+
};
|
|
760
781
|
if (verb === 'unpublish') {
|
|
761
|
-
requireArgs([sub, ...rest].filter(Boolean), 1, 1, 'teams unpublish SURFACE --room ROOM --workplace ORIGIN --organization ORG');
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
if (
|
|
773
|
-
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
+
requireArgs([sub, ...rest].filter(Boolean), 1, 1, 'teams unpublish SURFACE --room ROOM --workplace ORIGIN --organization ORG [--machine NAME] [--record-only|--workplace-only]');
|
|
783
|
+
// This server's record goes first: the moment it does, no delegated token opens the surface, whatever the workplace
|
|
784
|
+
// does next. Named --machine, only that machine; otherwise every machine of the team this caller may read the
|
|
785
|
+
// publications of (its own; all of them for an admin).
|
|
786
|
+
const machineIds = flags.machine ? [machine] : ((await team.machines.list()).items ?? []).map(item => item.id);
|
|
787
|
+
const matching = [];
|
|
788
|
+
for (const id of machineIds) {
|
|
789
|
+
let items;
|
|
790
|
+
try { items = (await client.request('GET', publicationsOf(id))).items ?? []; } catch (error) { if (flags.machine || !['forbidden', 'not_found'].includes(error?.code)) throw error; continue; }
|
|
791
|
+
for (const item of items) if (item.workplace === workplace && item.room === flags.room && (item.surface_key === sub || item.id === sub)) matching.push(item);
|
|
792
|
+
}
|
|
793
|
+
if (new Set(matching.map(item => item.machine_id)).size > 1) usage(`${sub} is published from more than one machine: name the publication id, or --machine`);
|
|
794
|
+
if (!matching.length && !flags.workplace_only) throw Object.assign(new Error(`this server holds no publication ${sub} for that Room and workplace (name the publication id, or --machine; --workplace-only retires it in the workplace alone)`), { code: 'not_found' });
|
|
795
|
+
for (const item of matching) await client.request('DELETE', `${publicationsOf(item.machine_id)}/${encodeURIComponent(item.id)}`);
|
|
796
|
+
if (recordOnly) return output({ version: 1, context: scope(context), data: { revoked: matching.map(item => item.id) } });
|
|
797
|
+
const payload = await workplaceCall(`${roomPath}/${encodeURIComponent(sub)}/retire`, {});
|
|
798
|
+
return output({ version: 1, context: scope(context), data: { ...(payload?.data && typeof payload.data === 'object' ? payload.data : {}), revoked: matching.map(item => item.id) } });
|
|
799
|
+
}
|
|
800
|
+
if (sub !== 'pane' && sub !== 'root') usage('teams publish pane CONTEXT_KEY|root PATH --room ROOM --workplace ORIGIN --organization ORG [--name NAME] [--key KEY] [--machine NAME] [--record-only]');
|
|
801
|
+
requireArgs(rest, 1, 1, `teams publish ${sub}`);
|
|
802
|
+
// A folder on this machine is its real path; on another machine, the path as given there.
|
|
803
|
+
const target = sub === 'pane' ? `pane:${rest[0]}` : `root:${flags.machine ? rest[0] : realpathSync(resolve(rest[0]))}`;
|
|
804
|
+
if (sub === 'root' && !target.startsWith('root:/')) usage('a folder on another machine is named by its absolute path');
|
|
805
|
+
if (sub === 'root' && target === 'root:/') usage('publish a folder, not the whole filesystem');
|
|
806
|
+
const label = sub === 'pane' ? rest[0] : basename(target.slice(5)) || 'root';
|
|
807
|
+
const key = flags.key ?? `${sub === 'pane' ? 'pane' : 'folder'}-${label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || createHash('sha256').update(target).digest('hex').slice(0, 8)}`;
|
|
808
|
+
const kind = sub === 'pane' ? 'pane' : 'folder';
|
|
809
|
+
const existed = (await recorded()).some(item => item.target === target && item.workplace === workplace && item.room === flags.room);
|
|
810
|
+
const publication = (await client.request('POST', publications, { body: { kind, target, workplace, room: flags.room, surface_key: key } })).data;
|
|
811
|
+
if (recordOnly) return output({ version: 1, context: scope(context), data: { publication } });
|
|
812
|
+
let payload;
|
|
813
|
+
try {
|
|
814
|
+
payload = await workplaceCall(roomPath, { config: { machine, target, team: context.team_id, teamsUrl: new URL(context.origin).origin }, key, kind, name: flags.name ?? label });
|
|
815
|
+
} catch (error) {
|
|
816
|
+
// Nothing is left published here that the workplace does not show.
|
|
817
|
+
if (!existed) await client.request('DELETE', `${publications}/${encodeURIComponent(publication.id)}`).catch(() => {});
|
|
818
|
+
throw error;
|
|
819
|
+
}
|
|
820
|
+
return output({ version: 1, context: scope(context), data: payload?.data && typeof payload.data === 'object' ? { ...payload.data, publication } : { surface: payload?.data ?? null, publication } });
|
|
782
821
|
}
|
|
783
822
|
|
|
784
823
|
async function listQuery(flags, { client, context, team }) {
|
|
@@ -874,8 +913,8 @@ function printHelp() {
|
|
|
874
913
|
login [ORIGIN] [--team TEAM] [--as CONTEXT] [--token-stdin|--invite-stdin] (Volter sign-in in the browser by default)
|
|
875
914
|
identity link
|
|
876
915
|
server start [--volter-issuer ORIGIN] [--delegated-issuer ORIGIN]
|
|
877
|
-
publish pane|root TARGET --room ROOM --workplace ORIGIN --organization ORG [--name] [--key] [--machine] [--token-stdin]
|
|
878
|
-
unpublish SURFACE --room ROOM --workplace ORIGIN --organization ORG [--token-stdin]
|
|
916
|
+
publish pane|root TARGET --room ROOM --workplace ORIGIN --organization ORG [--name] [--key] [--machine] [--token-stdin] [--record-only]
|
|
917
|
+
unpublish SURFACE --room ROOM --workplace ORIGIN --organization ORG [--machine] [--token-stdin] [--record-only|--workplace-only]
|
|
879
918
|
connect|disconnect|status
|
|
880
919
|
members invite|list|remove|role
|
|
881
920
|
groups list|create|remove|add-member|remove-member
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.29",
|
|
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",
|
|
@@ -53,9 +53,7 @@
|
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=20"
|
|
55
55
|
},
|
|
56
|
-
"scripts": {
|
|
57
|
-
"typecheck:teams": "tsc --noEmit -p test/tsconfig.json"
|
|
58
|
-
},
|
|
56
|
+
"scripts": {},
|
|
59
57
|
"dependencies": {
|
|
60
58
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.14.1",
|
|
61
59
|
"@volter/supercode-frontend": "^0.2.2",
|