@volter/supercode-teams 0.2.162 → 0.2.164
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 +4 -4
- package/bin/workspace/args.mjs +2 -2
- package/bin/workspace.mjs +23 -1
- package/package.json +1 -1
- package/server/capture-transactions.mjs +31 -0
- package/server/event-routes.mjs +4 -0
- package/server/harness-routes.mjs +30 -14
- package/server/index.mjs +3 -2
- package/server/store-core.mjs +7 -1
- package/worker/index.mjs +3 -4
- package/worker/wrangler.toml +2 -12
- package/workspace-client.d.ts +15 -0
- package/workspace-client.mjs +13 -0
package/README.md
CHANGED
|
@@ -46,10 +46,10 @@ a kind another product serves on the same machine. `title.set` needs `control` o
|
|
|
46
46
|
`observe` on a `root:/path` reads it (stat, list, read, search); `files` also changes it.
|
|
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
|
-
server
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
server trusts only admin-consented [app installations](../../docs/guides/teams-apps.md) with
|
|
50
|
+
`delegation.surfaces` or `delegation.sessions` for that team. Register the exact issuer origin
|
|
51
|
+
on the app; install the needed module separately. Operator issuer allowlists grant no access.
|
|
52
|
+
Keys are cached separately per origin, and each token must match a
|
|
53
53
|
publication or session binding for that exact issuer. Staging never opens a production
|
|
54
54
|
binding or publication. Tokens use Ed25519 (keys at `ORIGIN/.well-known/jwks.json`, audience this server's
|
|
55
55
|
public origin), each naming a pane (`pane:<key>`) or a folder (`root:/path`) on one machine of one
|
package/bin/workspace/args.mjs
CHANGED
|
@@ -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', '
|
|
14
|
-
'conversation', 'rh2', 'scope', 'agent',
|
|
13
|
+
'on', 'timeout', 'plan', 'tmux-socket', 'volter-issuer', 'workplace', 'organization', 'key', 'room',
|
|
14
|
+
'conversation', 'rh2', 'scope', 'agent', 'body-file',
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
17
|
export function parseArgs(argv) {
|
package/bin/workspace.mjs
CHANGED
|
@@ -57,6 +57,7 @@ export async function workspaceMain(argv = process.argv.slice(2), env = process.
|
|
|
57
57
|
const team = client.team(context.team_id);
|
|
58
58
|
const command = { flags, rest, output, client, context, team, explicitContext, home, contextName: selected, env, dependencies };
|
|
59
59
|
if (teamsPrefix && ['list', 'create'].includes(verb)) return teamsCommand(verb, sub, command);
|
|
60
|
+
if (verb === 'apps' || verb === 'app-installations') return appsCommand(verb,sub,{...command,output:value=>output(flags.json?value:redactAppSecrets(value))});
|
|
60
61
|
if (verb === 'sessions') return sessionsCommand(sub, command);
|
|
61
62
|
if (verb === 'members') return membersCommand(sub, command);
|
|
62
63
|
if (verb === 'groups') return groupsCommand(sub, command);
|
|
@@ -69,6 +70,27 @@ export async function workspaceMain(argv = process.argv.slice(2), env = process.
|
|
|
69
70
|
usage(`unsupported workspace command ${verb}`);
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function redactAppSecrets(value) {
|
|
74
|
+
if(!value||typeof value!=='object')return value;
|
|
75
|
+
if(Array.isArray(value))return value.map(redactAppSecrets);
|
|
76
|
+
return Object.fromEntries(Object.entries(value).filter(([key])=>!['secret','webhook_secret','code','token'].includes(key)).map(([key,item])=>[key,redactAppSecrets(item)]));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function appsCommand(verb,sub,{flags,rest,output,team}) {
|
|
80
|
+
only(flags,['json','help','context','body_file']);
|
|
81
|
+
const body=()=>{if(!flags.body_file)usage('--body-file is required (use - for stdin)');return JSON.parse(readFileSync(flags.body_file==='-'?0:flags.body_file,'utf8'));};
|
|
82
|
+
if(verb==='apps') {
|
|
83
|
+
if(sub==='catalogue'||sub==='list'){requireArgs(rest,0,0,'teams apps catalogue|list');return output(await team.apps[sub]());}
|
|
84
|
+
if(sub==='get'||sub==='rotate-secret'){requireArgs(rest,1,1,'teams apps get|rotate-secret APP');return output(await team.apps[sub==='get'?'get':'rotateSecret'](rest[0]));}
|
|
85
|
+
if(sub==='register'){requireArgs(rest,0,0,'teams apps register --body-file FILE');return output(await team.apps.register(body()));}
|
|
86
|
+
if(sub==='install'){requireArgs(rest,1,1,'teams apps install APP --body-file FILE');return output(await team.apps.install(rest[0],body()));}
|
|
87
|
+
} else {
|
|
88
|
+
if(sub==='list'){requireArgs(rest,0,0,'teams app-installations list');return output(await team.appInstallations.list());}
|
|
89
|
+
if(['get','uninstall','delegate'].includes(sub)){requireArgs(rest,1,1,'teams app-installations get|uninstall|delegate INSTALLATION');return output(await team.appInstallations[sub](rest[0],...(sub==='delegate'?[body()]:[])));}
|
|
90
|
+
}
|
|
91
|
+
usage('teams apps catalogue|list|get|register|install|rotate-secret; app-installations list|get|uninstall|delegate');
|
|
92
|
+
}
|
|
93
|
+
|
|
72
94
|
async function teamsCommand(verb, sub, { flags, rest, output, client }) {
|
|
73
95
|
if (verb === 'list') { only(flags, ['json', 'help', 'context', 'cursor', 'limit']); requireArgs(rest, 0, 0, 'teams list'); return output(await client.teams({ cursor: flags.cursor, limit: positiveInteger(flags.limit, 50, 'limit') })); }
|
|
74
96
|
const names = [sub, ...rest].filter(value => value !== undefined);
|
|
@@ -120,7 +142,7 @@ async function serverCommand({ flags, sub, rest, home, output }) {
|
|
|
120
142
|
// The Node host's store is node:sqlite (Node 22.5+); only this verb loads it, so every client
|
|
121
143
|
// verb runs on the Node 20 the package declares.
|
|
122
144
|
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,
|
|
145
|
+
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, ...(flags.init_team ? { initTeam: flags.init_team } : {}) });
|
|
124
146
|
output(await server.start());
|
|
125
147
|
await new Promise(resolveStop => { const stop = async () => { await server.close(); resolveStop(); }; process.once('SIGINT', stop); process.once('SIGTERM', stop); });
|
|
126
148
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.164",
|
|
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",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { MESSAGE_AT, OPEN_REVISION } from '@volter/teams/server/migrations';
|
|
3
|
+
import { normalizeSearch, textFromContent } from './catalog.mjs';
|
|
4
|
+
|
|
5
|
+
export function applyCaptureMessages({ get, run }, capture, revision) {
|
|
6
|
+
const { teamId, sessionId, sourceId, captureId, harness, nativeId, currentRevision, carriedCount, parts, messageNamespace } = capture;
|
|
7
|
+
let ordinal = carriedCount;
|
|
8
|
+
for (let index = 0; index < parts; index++) {
|
|
9
|
+
const heldPart = get('SELECT payload_json FROM capture_parts WHERE team_id=? AND capture_id=? AND part_index=?', [teamId, captureId, index]);
|
|
10
|
+
if (!heldPart) throw Object.assign(new Error('capture parts incomplete'), { status: 409, code: 'batch_gap' });
|
|
11
|
+
const part = JSON.parse(heldPart.payload_json);
|
|
12
|
+
for (const message of part.messages) {
|
|
13
|
+
const value = JSON.stringify(message.value);
|
|
14
|
+
const old = currentRevision == null ? null : get(`SELECT revision,source_id,native_key,recorded_at,value_json,until_revision FROM messages m WHERE m.team_id=? AND m.session_id=? AND m.ordinal=? AND ${MESSAGE_AT()} LIMIT 1`, [teamId, sessionId, ordinal, currentRevision, currentRevision]);
|
|
15
|
+
if (old && old.source_id === sourceId && old.native_key === message.native_key && old.recorded_at === message.recorded_at && old.value_json === value) {
|
|
16
|
+
if (old.until_revision !== OPEN_REVISION) run('UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND revision=? AND ordinal=?', [OPEN_REVISION, teamId, sessionId, old.revision, ordinal]);
|
|
17
|
+
ordinal++;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (old?.until_revision === OPEN_REVISION) run('UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND revision=? AND ordinal=?', [revision, teamId, sessionId, old.revision, ordinal]);
|
|
21
|
+
let messageId = get('SELECT id FROM message_ids WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND native_key=?', [teamId, sourceId, harness, nativeId, message.native_key])?.id;
|
|
22
|
+
if (!messageId) {
|
|
23
|
+
messageId = `msg_${createHash('sha256').update(`${messageNamespace}\0${message.native_key}`).digest('hex').slice(0, 32)}`;
|
|
24
|
+
run('INSERT INTO message_ids(team_id,source_id,harness,native_id,native_key,id) VALUES (?,?,?,?,?,?)', [teamId, sourceId, harness, nativeId, message.native_key, messageId]);
|
|
25
|
+
}
|
|
26
|
+
run('INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text,until_revision) VALUES (?,?,?,?,?,?,?,?,?,?,?)', [teamId, sessionId, revision, ordinal++, messageId, sourceId, message.native_key, message.recorded_at, value, normalizeSearch(textFromContent(message.value.content)), OPEN_REVISION]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (currentRevision != null) run('UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND ordinal>=? AND revision<=? AND until_revision=?', [revision, teamId, sessionId, ordinal, currentRevision, OPEN_REVISION]);
|
|
30
|
+
return { messages: ordinal - carriedCount };
|
|
31
|
+
}
|
package/server/event-routes.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appAuthentication } from '@volter/teams/server/apps';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { validateListQuery } from '../contracts/index.mjs';
|
|
3
4
|
import { cursorHash, cursorToken } from './catalog.mjs';
|
|
@@ -82,6 +83,7 @@ export async function sessionNames(api, teamId, descriptor) {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
async function readForEvent(api, auth, ref, tail) {
|
|
86
|
+
if(!auth.bound&&!await appAuthentication(api,auth))throw Object.assign(new Error('app authorization changed'),{status:401});
|
|
85
87
|
const nextCursorToken = cursorToken();
|
|
86
88
|
const snapshot = await api.store.readSessionSnapshot({
|
|
87
89
|
...sessionArgs(api, auth, ref), tail,
|
|
@@ -121,6 +123,7 @@ export async function handleSessionEvents({ api, req, res, auth, id, url, reques
|
|
|
121
123
|
const stream = openStream(api, req, res, auth, requestId, fail);
|
|
122
124
|
if (after === null) stream.write({ type: 'snapshot', cursor: initial.event_cursor, revision: initial.revision, data: initial });
|
|
123
125
|
const enqueue = streamQueue(stream, async () => {
|
|
126
|
+
if(!auth.bound&&!await appAuthentication(api,auth))fail(401,'authentication_required','app authorization changed');
|
|
124
127
|
// This worker operation reads current credential, membership, groups and every constituent ACL together.
|
|
125
128
|
await api.store.authorizedSession(sessionArgs(api, auth, sessionId));
|
|
126
129
|
const replay = await api.store.query('SELECT revision,kind FROM journal WHERE team_id=? AND session_id=? AND revision>? ORDER BY revision', [auth.team_id, sessionId, delivered]);
|
|
@@ -153,6 +156,7 @@ async function catalogWindow(api, auth, query) {
|
|
|
153
156
|
{ kind: 'query', sql: 'SELECT s.*,v.descriptor_json,v.sort_at FROM sessions s LEFT JOIN session_versions v ON v.team_id=s.team_id AND v.session_id=s.id AND v.revision=s.revision WHERE s.team_id=? ORDER BY v.sort_at DESC,s.id ASC', params: [auth.team_id] },
|
|
154
157
|
{ kind: 'query', sql: 'SELECT * FROM session_grants WHERE team_id=?', params: [auth.team_id] },
|
|
155
158
|
]);
|
|
159
|
+
if(!auth.bound&&!await appAuthentication(api,auth))throw Object.assign(new Error('app authorization changed'),{status:401});
|
|
156
160
|
const now = api.clock.nowMs();
|
|
157
161
|
if (!membership?.active || !credential || credential.revoked_at != null || credential.expires_at != null && credential.expires_at <= now) throw Object.assign(new Error('authorization changed'), { status: 401 });
|
|
158
162
|
const byId = new Map(rows.map(row => [row.id, row]));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { installedIssuer } from '@volter/teams/server/apps';
|
|
1
2
|
// The harness's routes on a Teams server (company RFC 0008 §6): the session catalog,
|
|
2
3
|
// search, reads, access and correlations; exports, continuations, effects and the
|
|
3
4
|
// attachment relay; sync rules; OTLP ingest, captures and uploads. The neutral handler
|
|
@@ -10,8 +11,8 @@ import { createHash } from 'node:crypto';
|
|
|
10
11
|
import { bearer, hashToken, canRead, canManage, isAdmin } from '@volter/teams/server/auth';
|
|
11
12
|
import { storageReport } from './storage-report.mjs';
|
|
12
13
|
import { validateListQuery, validateSearchQuery, validateReadQuery, validateCaptureBegin, validateCapturePart, canonicalJson } from '../contracts/index.mjs';
|
|
13
|
-
import { normalizeSearch,
|
|
14
|
-
import {
|
|
14
|
+
import { normalizeSearch, cursorHash, cursorToken } from './catalog.mjs';
|
|
15
|
+
import { handleOtlpRoute, sha256, validateArtifactName } from './ingest.mjs';
|
|
15
16
|
import { handleCatalogEvents, handleSessionEvents, sessionNames } from './event-routes.mjs';
|
|
16
17
|
import { exact } from '@volter/teams/server/request-json';
|
|
17
18
|
import { MESSAGE_AT, OPEN_REVISION } from '@volter/teams/server/migrations';
|
|
@@ -328,7 +329,26 @@ async function artifactChunk(api,req,res,auth,sourceId,uploadId,index,requestId)
|
|
|
328
329
|
],{status:200,body:response,targetId:uploadId});
|
|
329
330
|
send(res,200,response,requestId);
|
|
330
331
|
}
|
|
331
|
-
async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){const source=await sourceAuth(api,auth,sourceId);const commit=await parseJson(req);exact(commit,['capture_id','parts','digest','title','repository','reported_user','recorded_activity_at','parent_native_id','coverage','artifacts','native_bundle']);if(commit.capture_id!==captureId||!Number.isSafeInteger(commit.parts)||commit.parts<1)fail(400,'invalid_request','invalid commit');const cap=await api.store.one('SELECT * FROM captures WHERE team_id=? AND id=? AND source_id=?',[auth.team_id,captureId,sourceId]);if(await expireStaging(api,auth.team_id,sourceId,captureId,'captures',cap))fail(409,'resync_required','capture reservation expired');if(!cap)fail(404,'not_found','capture not found');if(cap.state==='committed'){const receipt=JSON.parse(cap.receipt_json);if(receipt.digest!==commit.digest)fail(409,'capture_conflict','commit replay differs');receipt.duplicate=true;return send(res,200,result(api,auth,receipt),requestId);}const
|
|
332
|
+
async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){const source=await sourceAuth(api,auth,sourceId);const commit=await parseJson(req);exact(commit,['capture_id','parts','digest','title','repository','reported_user','recorded_activity_at','parent_native_id','coverage','artifacts','native_bundle']);if(commit.capture_id!==captureId||!Number.isSafeInteger(commit.parts)||commit.parts<1)fail(400,'invalid_request','invalid commit');const cap=await api.store.one('SELECT * FROM captures WHERE team_id=? AND id=? AND source_id=?',[auth.team_id,captureId,sourceId]);if(await expireStaging(api,auth.team_id,sourceId,captureId,'captures',cap))fail(409,'resync_required','capture reservation expired');if(!cap)fail(404,'not_found','capture not found');if(cap.state==='committed'){const receipt=JSON.parse(cap.receipt_json);if(receipt.digest!==commit.digest)fail(409,'capture_conflict','commit replay differs');receipt.duplicate=true;return send(res,200,result(api,auth,receipt),requestId);}const begin=JSON.parse(cap.begin_json);if(source.kind==='connector'){const rule=await api.store.one('SELECT * FROM sync_rules WHERE team_id=? AND id=? AND source_id=? AND machine_id=?',[auth.team_id,begin.rule_id,sourceId,auth.machine_id]);if(!rule||rule.state!=='active'||rule.revision!==begin.rule_revision||rule.visibility!==begin.visibility)fail(rule?.state==='paused'?409:403,rule?.state==='paused'?'source_paused':'forbidden','sync rule changed before commit');}
|
|
333
|
+
const heldParts = await api.store.one('SELECT COUNT(*) count,MIN(part_index) first,MAX(part_index) last FROM capture_parts WHERE team_id=? AND capture_id=?', [auth.team_id, captureId]);
|
|
334
|
+
if (heldParts.count !== commit.parts || heldParts.first !== 0 || heldParts.last !== commit.parts - 1) fail(409, 'batch_gap', 'capture parts incomplete');
|
|
335
|
+
const unsignedCommit = { ...commit };
|
|
336
|
+
delete unsignedCommit.digest;
|
|
337
|
+
const captureHash = createHash('sha256').update(`{"begin":${canonicalJson(begin)},"commit":${canonicalJson(unsignedCommit)},"parts":[`);
|
|
338
|
+
const sentKeys = new Set();
|
|
339
|
+
let messageCount = 0;
|
|
340
|
+
for (let index = 0; index < commit.parts; index++) {
|
|
341
|
+
const part = await api.store.one('SELECT payload_json FROM capture_parts WHERE team_id=? AND capture_id=? AND part_index=?', [auth.team_id, captureId, index]);
|
|
342
|
+
if (!part) fail(409, 'batch_gap', 'capture parts incomplete');
|
|
343
|
+
if (index) captureHash.update(',');
|
|
344
|
+
captureHash.update(part.payload_json);
|
|
345
|
+
for (const message of JSON.parse(part.payload_json).messages) {
|
|
346
|
+
if (sentKeys.has(message.native_key)) fail(409, 'capture_conflict', 'duplicate native message key');
|
|
347
|
+
sentKeys.add(message.native_key);
|
|
348
|
+
if (++messageCount > 100000) fail(413, 'request_too_large', 'capture contains too many messages');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (captureHash.update(']}').digest('hex') !== commit.digest) fail(409, 'capture_conflict', 'capture digest differs');
|
|
332
352
|
// Native transcripts can be larger than the Worker's entire heap. Validate
|
|
333
353
|
// the retained bytes one chunk at a time; never materialize a whole artifact.
|
|
334
354
|
for (const manifest of commit.artifacts) {
|
|
@@ -358,9 +378,9 @@ async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){cons
|
|
|
358
378
|
if (begin.carry) {
|
|
359
379
|
const { count, digest } = begin.carry;
|
|
360
380
|
const session = origin ? await api.store.one('SELECT revision FROM sessions WHERE team_id=? AND id=?', [auth.team_id, origin.session_id]) : null;
|
|
361
|
-
if (!session || count +
|
|
381
|
+
if (!session || count + messageCount > 100000) fail(409, 'carry_mismatch', 'the carried prefix is unavailable or too large');
|
|
362
382
|
carriedRevision = session.revision;
|
|
363
|
-
const hash = createHash('sha256').update('[')
|
|
383
|
+
const hash = createHash('sha256').update('[');
|
|
364
384
|
while (carriedCount < count) {
|
|
365
385
|
const page = await api.store.query(`SELECT ordinal,source_id,native_key,recorded_at,value_json FROM messages m
|
|
366
386
|
WHERE m.team_id=? AND m.session_id=? AND m.ordinal>=? AND m.ordinal<? AND ${MESSAGE_AT()} ORDER BY m.ordinal LIMIT 16`,
|
|
@@ -368,7 +388,7 @@ async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){cons
|
|
|
368
388
|
if (!page.length) fail(409, 'carry_mismatch', 'the carried messages are not what this session holds; send the capture whole');
|
|
369
389
|
for (const row of page) {
|
|
370
390
|
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');
|
|
371
|
-
if (
|
|
391
|
+
if (sentKeys.has(row.native_key)) fail(409, 'capture_conflict', 'duplicate native message key');
|
|
372
392
|
if (carriedCount) hash.update(',');
|
|
373
393
|
hash.update(canonicalJson({native_key:row.native_key, recorded_at:row.recorded_at ?? null, value:JSON.parse(row.value_json)}));
|
|
374
394
|
carriedCount++;
|
|
@@ -385,15 +405,11 @@ async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){cons
|
|
|
385
405
|
// session that gained one message writes one row, not the whole session again.
|
|
386
406
|
const carryGuards=[],currentRevision=origin?(await api.store.one('SELECT revision FROM sessions WHERE team_id=? AND id=?',[auth.team_id,sid]))?.revision??null:null;
|
|
387
407
|
if (carriedRevision != null && currentRevision !== carriedRevision) fail(409, 'revision_mismatch', 'session changed during prefix verification; retry');
|
|
388
|
-
|
|
408
|
+
sentKeys.clear();
|
|
409
|
+
carryGuards.push({ sql: 'SELECT 1 ok WHERE (SELECT COUNT(*) FROM capture_parts WHERE team_id=? AND capture_id=?)=?', params: [auth.team_id, captureId, commit.parts], status: 409, code: 'batch_gap', message: 'capture parts changed before commit' });
|
|
389
410
|
// Another commit to this session between this read and the write would make the carried rows wrong: refused, retried.
|
|
390
411
|
if(currentRevision!=null)carryGuards.push({sql:'SELECT 1 ok FROM sessions WHERE team_id=? AND id=? AND revision=?',params:[auth.team_id,sid,currentRevision],code:'revision_mismatch',status:409,message:'session changed during capture; retry'});
|
|
391
|
-
|
|
392
|
-
let ordinal=carriedCount;for(const m of messages){const value=JSON.stringify(m.value),old=held.get(ordinal);held.delete(ordinal);
|
|
393
|
-
if(old&&old.source_id===sourceId&&old.native_key===m.native_key&&old.recorded_at===m.recorded_at&&old.value_json===value){if(old.until_revision!==OPEN_REVISION)statements.push({sql:'UPDATE messages SET until_revision=? WHERE team_id=? AND session_id=? AND revision=? AND ordinal=?',params:[OPEN_REVISION,auth.team_id,sid,old.revision,old.ordinal]});ordinal++;continue;}
|
|
394
|
-
if(old)close(old);
|
|
395
|
-
let mid=(await api.store.one('SELECT id FROM message_ids WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND native_key=?',[auth.team_id,sourceId,cap.harness,cap.native_id,m.native_key]))?.id;if(!mid){mid=api.ids.next('message');statements.push({sql:'INSERT INTO message_ids(team_id,source_id,harness,native_id,native_key,id) VALUES (?,?,?,?,?,?)',params:[auth.team_id,sourceId,cap.harness,cap.native_id,m.native_key,mid]});}statements.push({sql:'INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text,until_revision) VALUES (?,?,?,?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,ordinal++,mid,sourceId,m.native_key,m.recorded_at,value,normalizeSearch(textFromContent(m.value.content)),OPEN_REVISION]});}
|
|
396
|
-
for(const row of held.values())close(row);
|
|
412
|
+
statements.push({ kind: 'capture_messages', capture: { teamId: auth.team_id, sessionId: sid, sourceId, captureId, harness: cap.harness, nativeId: cap.native_id, currentRevision, carriedCount, parts: commit.parts, messageNamespace: api.ids.next('message') } });
|
|
397
413
|
const bundle=commit.native_bundle??null,legacyBundle=commit.artifacts.length?captureId:null;for(let index=0;index<commit.artifacts.length;index++){const a=commit.artifacts[index],file=bundle?.files.find(x=>x.upload_id===a.upload_id);statements.push({sql:'UPDATE uploads SET manifest_json=?,committed=1 WHERE team_id=? AND id=?',params:[JSON.stringify(a),auth.team_id,a.upload_id]},{sql:'INSERT INTO session_artifacts(team_id,session_id,revision,bundle_id,upload_id,format,provenance,file_role,fidelity,residue_json) VALUES (?,?,?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,bundle?.id??legacyBundle,a.upload_id,bundle?.harness??cap.harness,'capture',file?.role??(index===0?'primary':'bundle'),bundle?.fidelity??'semantic',JSON.stringify(bundle?.residue??['native_bundle_metadata_unavailable'])]});}const storedReceipt={source_id:sourceId,capture_id:captureId,session_id:sid,generation:cap.generation,sequence:cap.sequence,digest:commit.digest,revision:'__JOURNAL_REVISION__',duplicate:false};statements.push({sql:"UPDATE captures SET state='committed',commit_json=?,receipt_json=? WHERE team_id=? AND id=?",params:[JSON.stringify(commit),{$journalJsonValue:storedReceipt},auth.team_id,captureId]},{sql:'DELETE FROM capture_parts WHERE team_id=? AND capture_id=?',params:[auth.team_id,captureId]},{sql:'UPDATE sources SET last_received_at=? WHERE team_id=? AND id=?',params:[boundaryNow,auth.team_id,sourceId]},{sql:'UPDATE teams SET journal_revision=? WHERE id=?',params:[revisionParam,auth.team_id]},{sql:'INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',params:[auth.team_id,revisionParam,sid,'capture',boundaryNow]});const guards=[{sql:'SELECT 1 ok FROM credentials WHERE id=? AND source_id=? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at>?)',params:[auth.credential_id,sourceId,boundaryNow],code:'authentication_required',status:401,message:'source credential expired or revoked'},...lineageGuards,...carryGuards,{sql:"SELECT 1 ok FROM captures c JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_id LEFT JOIN machines m ON m.team_id=s.team_id AND m.id=s.machine_id LEFT JOIN sync_rules r ON r.team_id=c.team_id AND r.id=json_extract(c.begin_json,'$.rule_id') WHERE c.team_id=? AND c.id=? AND c.source_id=? AND c.state='staging' AND s.state='active' AND (s.kind!='connector' OR (m.revoked_at IS NULL AND r.machine_id=m.id AND r.source_id=s.id AND r.state='active' AND r.revision=json_extract(c.begin_json,'$.rule_revision') AND r.visibility=json_extract(c.begin_json,'$.visibility')))",params:[auth.team_id,captureId,sourceId],code:'conflict',status:409,message:'capture authorization changed'}],storedResponse=result(api,auth,storedReceipt),{revision}=await guardedMutation(api,res,auth,guards,statements,{status:200,body:storedResponse,targetId:captureId,journal:true}),receipt={...storedReceipt,revision};api.events.publish(auth.team_id,{type:'session_changed',cursor:String(revision),revision,session_id:sid,reason:'capture',read_required:true});send(res,200,result(api,auth,receipt),requestId);
|
|
398
414
|
}
|
|
399
415
|
|
|
@@ -409,7 +425,7 @@ async function viewBindingRoute(api,req,res,auth,ref,requestId){
|
|
|
409
425
|
if(session.custodian_id!==auth.principal_id)fail(403,'forbidden',"only the session's owner binds it to a view");
|
|
410
426
|
const body=await parseJson(req);exact(body,['issuer','room','conversation']);
|
|
411
427
|
if(typeof body.room!=='string'||!body.room||typeof body.conversation!=='string'||!body.conversation)fail(400,'invalid_request','room and conversation are required');
|
|
412
|
-
if(!api.
|
|
428
|
+
if(!await installedIssuer(api,body.issuer,'delegation.sessions',auth.team_id))fail(400,'invalid_request','this issuer is not an installed session integration');
|
|
413
429
|
const key=[auth.team_id,session.id,body.issuer,body.room,body.conversation];
|
|
414
430
|
await api.store.transaction(req.method==='POST'
|
|
415
431
|
?[{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,
|
|
19
|
+
export function createTeamsServer({dataDir,listen={host:'127.0.0.1',port:7331},publicOrigin=null,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,
|
|
24
|
+
const api=createTeamsApi({clock,ids,publicOrigin,identityOrganizations:identityOrganizationsFromEnv(process.env),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;
|
|
@@ -43,6 +43,7 @@ export function createTeamsServer({dataDir,listen={host:'127.0.0.1',port:7331},p
|
|
|
43
43
|
if(!existsSync(credentialPath)&&existsSync(pendingPath)){const pending=JSON.parse(readFileSync(pendingPath,'utf8'));if(pending.server_id!==serverId)throw new Error('bootstrap credential recovery does not match server identity');renameSync(pendingPath,credentialPath);chmodSync(credentialPath,0o600);}
|
|
44
44
|
}
|
|
45
45
|
api.attach({serverId});
|
|
46
|
+
if(existsSync(credentialPath))api.appRootSecret=JSON.parse(readFileSync(credentialPath,'utf8')).token;
|
|
46
47
|
const maintenance=async()=>{
|
|
47
48
|
let next=60000;
|
|
48
49
|
try{
|
package/server/store-core.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// them on the object's own SQLite.
|
|
6
6
|
import { ingestOtlpBatch, correlateSessions } from './arc2-transactions.mjs';
|
|
7
7
|
import { claimEffect, getEffect, listEffects, reportEffect } from './effects-transactions.mjs';
|
|
8
|
+
import { applyCaptureMessages } from './capture-transactions.mjs';
|
|
8
9
|
import { MIGRATIONS, MESSAGE_AT } from '@volter/teams/server/migrations';
|
|
9
10
|
|
|
10
11
|
/** Bring a database to the newest schema. `version()`/`setVersion(n)` name where the host records it. */
|
|
@@ -27,10 +28,15 @@ export function createStoreHandlers({ all, get, run, tx }) {
|
|
|
27
28
|
// session its owner bound to that Room and Conversation; it holds no credential and is no member.
|
|
28
29
|
let membership=null;
|
|
29
30
|
if(bound){
|
|
31
|
+
if(!get(`SELECT 1 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 WHERE i.team_id=? AND a.issuer=? AND i.state='active' AND a.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='delegation.sessions')`,[teamId,bound.issuer]))domainError(401,'authentication_required','integration was uninstalled');
|
|
30
32
|
if(!(bound.expiresAt>now))domainError(401,'authentication_required','authentication required');
|
|
31
33
|
}else{
|
|
32
34
|
const credential=get("SELECT id FROM credentials WHERE id=? AND principal_id=? AND kind IN ('user','service') AND (team_id IS NULL OR team_id=?) AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at>?)",[credentialId,principalId,teamId,now]);
|
|
33
35
|
membership=get('SELECT role FROM memberships WHERE team_id=? AND principal_id=? AND active=1',[teamId,principalId]);
|
|
36
|
+
const identity=get(`SELECT i.state,a.state app_state,m.active,m.role FROM identity_app_credentials c JOIN app_installations i ON i.id=c.installation_id JOIN apps a ON a.id=i.app_id JOIN memberships m ON m.team_id=i.team_id AND m.principal_id=i.installer_id WHERE c.credential_id=?`,[credentialId]);
|
|
37
|
+
if(identity&&(identity.state!=='active'||identity.app_state!=='active'||!identity.active||!['owner','admin'].includes(identity.role)))domainError(401,'authentication_required','identity integration revoked');
|
|
38
|
+
const app=get('SELECT ac.*,i.state,a.state app_state,installer.active,installer.role FROM app_credentials ac JOIN app_installations i ON i.id=ac.installation_id JOIN apps a ON a.id=i.app_id JOIN memberships installer ON installer.team_id=i.team_id AND installer.principal_id=i.installer_id WHERE ac.credential_id=?',[credentialId]);
|
|
39
|
+
if(app){if(app.state!=='active'||app.app_state!=='active'||!app.active||!['owner','admin'].includes(app.role)||!JSON.parse(app.actions_json).includes('sessions.read'))domainError(403,'forbidden','app session read authorization changed');const revoked=get(`WITH RECURSIVE lineage(id,parent_id) AS (SELECT credential_id,parent_id FROM app_credentials WHERE credential_id=? UNION ALL SELECT ac.credential_id,ac.parent_id FROM app_credentials ac JOIN lineage l ON ac.credential_id=l.parent_id) SELECT 1 FROM lineage l JOIN credentials c ON c.id=l.id WHERE c.revoked_at IS NOT NULL OR c.expires_at<=? LIMIT 1`,[credentialId,now]);if(revoked)domainError(401,'authentication_required','parent credential revoked');}
|
|
34
40
|
if(!credential||!membership)domainError(401,'authentication_required','authentication required');
|
|
35
41
|
}
|
|
36
42
|
if(typeof ref!=='string'||ref.length<8)domainError(404,'not_found','not found');
|
|
@@ -119,7 +125,7 @@ export function createStoreHandlers({ all, get, run, tx }) {
|
|
|
119
125
|
transaction: ({statements}) => tx(() => statements.map(s => s.kind === 'query' ? all(s.sql,s.params) : s.kind === 'one' ? get(s.sql,s.params) : run(s.sql,s.params))),
|
|
120
126
|
snapshot: ({statements}) => tx(() => statements.map(s => s.kind === 'one' ? get(s.sql,s.params) : all(s.sql,s.params))),
|
|
121
127
|
guardedTransaction: ({guards=[],statements}) => tx(() => { for(const guard of guards){const row=get(guard.sql,guard.params);if(!row){const error=new Error(guard.message??'transaction predicate changed');error.code=guard.code??'conflict';error.status=guard.status??409;throw error;}}return statements.map(s=>s.kind==='query'?all(s.sql,s.params):s.kind==='one'?get(s.sql,s.params):run(s.sql,s.params));}),
|
|
122
|
-
journalTransaction: ({teamId,guards=[],statements}) => tx(() => { for(const guard of guards){const row=get(guard.sql,guard.params);if(!row){const error=new Error(guard.message??'transaction predicate changed');error.code=guard.code??'conflict';error.status=guard.status??409;throw error;}}const team=get('SELECT journal_revision FROM teams WHERE id=?',[teamId]);if(!team)throw new Error('team not found');const revision=team.journal_revision+1;const results=statements.map(s=>{const params=(s.params??[]).map(value=>journalParam(value,revision));return s.kind==='query'?all(s.sql,params):s.kind==='one'?get(s.sql,params):run(s.sql,params);});return{revision,results};}),
|
|
128
|
+
journalTransaction: ({teamId,guards=[],statements}) => tx(() => { for(const guard of guards){const row=get(guard.sql,guard.params);if(!row){const error=new Error(guard.message??'transaction predicate changed');error.code=guard.code??'conflict';error.status=guard.status??409;throw error;}}const team=get('SELECT journal_revision FROM teams WHERE id=?',[teamId]);if(!team)throw new Error('team not found');const revision=team.journal_revision+1;const results=statements.map(s=>{if(s.kind==='capture_messages')return applyCaptureMessages({get,run},s.capture,revision);const params=(s.params??[]).map(value=>journalParam(value,revision));return s.kind==='query'?all(s.sql,params):s.kind==='one'?get(s.sql,params):run(s.sql,params);});return{revision,results};}),
|
|
123
129
|
ingestOtlpBatch: args => tx(() => ingestOtlpBatch({get,all,run},args)),
|
|
124
130
|
correlateSessions: args => tx(() => correlateSessions({get,all,run},args)),
|
|
125
131
|
listEffects: args => tx(() => listEffects({get,all,run},args)),
|
package/worker/index.mjs
CHANGED
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
// Configuration (wrangler.toml [vars] and secrets):
|
|
6
6
|
// PUBLIC_ORIGIN the https origin the Worker is served at
|
|
7
7
|
// INIT_TEAM the first team's name (used once, at bootstrap)
|
|
8
|
-
//
|
|
8
|
+
// Identity and delegation issuers are registered apps installed by team admins through the public doors.
|
|
9
9
|
// VOLTER_ORG_CLIENT_ID, VOLTER_ORG_CLIENT_SECRET, VOLTER_TEAM_ORGS
|
|
10
|
-
// optional: this server's product credential at
|
|
10
|
+
// optional: this server's product credential at the organization's identity issuer, and the teams whose
|
|
11
11
|
// organization there admits its members with no invitation (`team=slug,…`)
|
|
12
|
-
// DELEGATED_ISSUER optional: a workplace (RH2) whose delegated tokens open one pane or folder at /view
|
|
13
12
|
// SUPERCODE_TEAMS_BOOTSTRAP_TOKEN secret: the owner's first credential, minted with
|
|
14
13
|
// `supercode teams server token`
|
|
15
14
|
import { DurableObject } from 'cloudflare:workers';
|
|
@@ -81,7 +80,7 @@ export class TeamsServer extends DurableObject {
|
|
|
81
80
|
console.log(`storage: database_bytes=${ctx.storage.sql.databaseSize}`);
|
|
82
81
|
const db = durableSql(ctx.storage);
|
|
83
82
|
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),
|
|
83
|
+
this.api = createTeamsApi({ publicOrigin: normalizePublicOrigin(env.PUBLIC_ORIGIN), appRootSecret: env.SUPERCODE_TEAMS_BOOTSTRAP_TOKEN || null, identityOrganizations: identityOrganizationsFromEnv(env) });
|
|
85
84
|
this.api.storageUsage = () => {
|
|
86
85
|
const sql = ctx.storage.sql;
|
|
87
86
|
let freeBytes = null;
|
package/worker/wrangler.toml
CHANGED
|
@@ -4,24 +4,14 @@
|
|
|
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
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# PUBLIC_ORIGIN the https origin the Worker is served at (required)
|
|
10
|
-
# INIT_TEAM the first team's name, used once at bootstrap
|
|
11
|
-
# VOLTER_ISSUER optional: members sign in with Volter (supercode teams login --volter); the issuer
|
|
12
|
-
# must have this origin registered as a resource
|
|
13
|
-
# DELEGATED_ISSUER optional: a workplace (RH2) whose delegated tokens open published panes and folders and
|
|
14
|
-
# bound sessions; its keys are at <issuer>/.well-known/jwks.json
|
|
15
|
-
# DELEGATED_ISSUERS comma-separated exact origins, additive with legacy DELEGATED_ISSUER
|
|
7
|
+
# Identity and delegated access are admin-consented app installations, never operator allowlists.
|
|
8
|
+
# PUBLIC_ORIGIN and INIT_TEAM supply the listener and bootstrap team.
|
|
16
9
|
name = "supercode-teams"
|
|
17
10
|
main = "index.mjs"
|
|
18
11
|
compatibility_date = "2026-09-01"
|
|
19
12
|
compatibility_flags = ["nodejs_compat"]
|
|
20
13
|
keep_vars = true
|
|
21
14
|
|
|
22
|
-
[vars]
|
|
23
|
-
DELEGATED_ISSUERS = "https://pilot.runhuman.com,https://rh2-staging.voltertest.xyz"
|
|
24
|
-
|
|
25
15
|
# Workers Logs: the Worker's log lines (viewer-error, session-view refused, maintain) kept and searchable after the fact.
|
|
26
16
|
[observability]
|
|
27
17
|
enabled = true
|
package/workspace-client.d.ts
CHANGED
|
@@ -36,9 +36,24 @@ export declare class TeamsClient {
|
|
|
36
36
|
capabilities(options?:RequestOptions):Promise<Capabilities>;
|
|
37
37
|
teams(query?:{limit?:number;cursor?:string},options?:RequestOptions):Promise<ServerPage<Team>>;
|
|
38
38
|
createTeam(body:{name:string},options?:RequestOptions):Promise<Result<Team>>;
|
|
39
|
+
redeemAppCode(body:{app_id:string;app_secret:string;code:string;redirect_url:string},options?:RequestOptions):Promise<AppCodeExchange>;
|
|
39
40
|
team(id:string):TeamClient;
|
|
40
41
|
}
|
|
42
|
+
export interface TeamsApp {
|
|
43
|
+
id:string; team_id:string; principal_id:string; name:string; description:string;
|
|
44
|
+
actions:string[]; event_kinds:string[]; redirect_urls:string[]; webhook_url:string|null;
|
|
45
|
+
issuer:string|null; state:'active'|'retired'; created_at:number;
|
|
46
|
+
}
|
|
47
|
+
export type RegisterTeamsApp=Pick<TeamsApp,'name'|'actions'|'redirect_urls'> & Partial<Pick<TeamsApp,'description'|'event_kinds'|'webhook_url'|'issuer'>>;
|
|
48
|
+
export interface AppInstallation {
|
|
49
|
+
id:string; app_id:string; team_id:string; principal_id:string; installer_id:string;
|
|
50
|
+
actions:string[]; state:'active'|'uninstalled'; created_at:number; uninstalled_at:number|null;
|
|
51
|
+
}
|
|
52
|
+
export interface AppToken { token:string; kind:'service'; expires_at:number; actions:string[]; subject:string; act:string[] }
|
|
53
|
+
export interface AppCodeExchange { version:1; installation:AppInstallation; credential:AppToken; webhook_secret:string }
|
|
41
54
|
export interface TeamClient {
|
|
55
|
+
apps:{catalogue(options?:RequestOptions):Promise<Result<{actions:string[];event_kinds:string[]}>>;list(options?:RequestOptions):Promise<Page<TeamsApp>>;get(id:string,options?:RequestOptions):Promise<Result<TeamsApp>>;register(body:RegisterTeamsApp,options?:RequestOptions):Promise<Result<TeamsApp & {secret?:string}>>;rotateSecret(id:string,options?:RequestOptions):Promise<Result<{id:string;secret?:string}>>;install(id:string,body:{redirect_url:string;state:string;actions?:string[]},options?:RequestOptions):Promise<Result<{installation:AppInstallation;code?:string;state:string;redirect_url:string;expires_at:number}>>};
|
|
56
|
+
appInstallations:{list(options?:RequestOptions):Promise<Page<AppInstallation>>;get(id:string,options?:RequestOptions):Promise<Result<AppInstallation>>;uninstall(id:string,options?:RequestOptions):Promise<null>;delegate(id:string,body:{actions:string[];actor:string;expires_in_ms?:number},options?:RequestOptions):Promise<Result<AppToken|null>>};
|
|
42
57
|
sessions:{list(query?:ListQuery,options?:RequestOptions):Promise<Page<Descriptor>>;search(query:SearchQuery,options?:RequestOptions):Promise<SearchPage>;watch(query?:ListQuery,options?:StreamOptions):AsyncIterable<Event>;get(id:string):SessionClient};
|
|
43
58
|
members:{list(query?:Record<string,unknown>,options?:RequestOptions):Promise<Page<Membership>>;update(id:string,body:{role:Membership['role']},options?:RequestOptions):Promise<Result<Membership>>;remove(id:string,options?:RequestOptions):Promise<null>};
|
|
44
59
|
groups:{list(query?:Record<string,unknown>,options?:RequestOptions):Promise<Page<Group>>;create(body:{name:string},options?:RequestOptions):Promise<Result<Group>>;remove(id:string,options?:RequestOptions):Promise<null>;addMember(groupId:string,memberId:string,options?:RequestOptions):Promise<null>;removeMember(groupId:string,memberId:string,options?:RequestOptions):Promise<null>};
|
package/workspace-client.mjs
CHANGED
|
@@ -12,6 +12,7 @@ export class TeamsClient {
|
|
|
12
12
|
throw last;
|
|
13
13
|
}
|
|
14
14
|
me(options){return this.request('GET','/api/v1/me',options);} capabilities(options){return this.request('GET','/api/v1/capabilities',options);} team(id){return new TeamClient(this,id);}
|
|
15
|
+
redeemAppCode(body,options){return this.request('POST','/api/v1/apps/token',{...options,body});}
|
|
15
16
|
teams(queryValue={},options){return this.request('GET','/api/v1/teams'+query(queryValue),options);}
|
|
16
17
|
createTeam(body,options){return this.request('POST','/api/v1/teams',{...options,body});}
|
|
17
18
|
}
|
|
@@ -28,6 +29,18 @@ class TeamClient {
|
|
|
28
29
|
const patch = (path, body, options) => root.request('PATCH', base + path, { ...options, body });
|
|
29
30
|
const remove = (path, options) => root.request('DELETE', base + path, options);
|
|
30
31
|
const resource = (path, id) => `${path}/${segment(id)}`;
|
|
32
|
+
this.apps = {
|
|
33
|
+
catalogue: o => get('/apps/catalogue', o),
|
|
34
|
+
list: o => get('/apps', o), get: (id, o) => get(resource('/apps', id), o),
|
|
35
|
+
register: (body, o) => post('/apps', body, o),
|
|
36
|
+
rotateSecret: (id, o) => post(resource('/apps', id) + '/secret', {}, o),
|
|
37
|
+
install: (id, body, o) => post(resource('/apps', id) + '/installations', body, o),
|
|
38
|
+
};
|
|
39
|
+
this.appInstallations = {
|
|
40
|
+
list: o => get('/app-installations', o), get: (id, o) => get(resource('/app-installations', id), o),
|
|
41
|
+
uninstall: (id, o) => remove(resource('/app-installations', id), o),
|
|
42
|
+
delegate: (id, body, o) => post(resource('/app-installations', id) + '/delegations', body, o),
|
|
43
|
+
};
|
|
31
44
|
this.sessions = {
|
|
32
45
|
list: (q = {}, o) => get('/sessions' + query(q), o),
|
|
33
46
|
search: (q, o) => get('/sessions/search' + query(q), o),
|