@kybernesis/buzz 0.5.0 → 0.5.1
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/dist/bridge.js +33 -12
- package/dist/credentials.d.ts +25 -0
- package/dist/credentials.js +36 -0
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { channelIdentity } from "@kybernesis/enterprise";
|
|
2
|
+
import { speakerCredentials } from "./credentials.js";
|
|
2
3
|
import { Client } from "eve/client";
|
|
3
4
|
import { BuzzRelay } from "./relay.js";
|
|
4
5
|
import { loadKey, npubEncode } from "./keys.js";
|
|
@@ -50,14 +51,33 @@ export function buzzBridge(options) {
|
|
|
50
51
|
const sessions = new Map();
|
|
51
52
|
/** Who has already been sent a link, so a room full of strangers is not a room full of spam. */
|
|
52
53
|
const invited = new Map();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
/**
|
|
55
|
+
* A client whose credentials are resolved per request, not captured once.
|
|
56
|
+
*
|
|
57
|
+
* The identity token a turn starts with lives about five minutes. That was
|
|
58
|
+
* invisible while turns ended early — and became a failure the moment they
|
|
59
|
+
* were allowed to run to completion: a turn doing real work for six minutes
|
|
60
|
+
* reconnected its stream with the token it began with, the agent answered
|
|
61
|
+
* "Authorization is required for this route", and the reply was lost after
|
|
62
|
+
* all the work had been done. In a channel that reads as the agent typing
|
|
63
|
+
* for five minutes and then saying nothing.
|
|
64
|
+
*
|
|
65
|
+
* Resolving per request costs nothing in the normal case — the resolver
|
|
66
|
+
* caches and re-mints only near expiry — and it keeps the property the short
|
|
67
|
+
* lifetime exists for: a person whose access is revoked mid-turn stops being
|
|
68
|
+
* able to act on the next request rather than at the end of the turn.
|
|
69
|
+
*/
|
|
70
|
+
const credentialsFor = speakerCredentials((externalId) => identity.resolve(PROVIDER, externalId, npubEncode(externalId)));
|
|
71
|
+
const clientFor = (pubkey) => {
|
|
72
|
+
const credentials = credentialsFor(pubkey);
|
|
73
|
+
// `host`, not `baseUrl`: a wrong key is ignored rather than rejected, and the client then
|
|
74
|
+
// builds a nonsense URL — an error about a malformed URL rather than about a mistyped option.
|
|
75
|
+
return new Client({
|
|
76
|
+
host: options.agentUrl,
|
|
77
|
+
auth: { bearer: credentials.bearer },
|
|
78
|
+
headers: credentials.headers,
|
|
79
|
+
});
|
|
80
|
+
};
|
|
61
81
|
/**
|
|
62
82
|
* One turn at a time per channel.
|
|
63
83
|
*
|
|
@@ -73,9 +93,10 @@ export function buzzBridge(options) {
|
|
|
73
93
|
inFlight.set(channel, queued.catch(() => { }));
|
|
74
94
|
return queued;
|
|
75
95
|
}
|
|
76
|
-
async function ask(channel, text,
|
|
77
|
-
// Built per turn, for the person whose turn it is
|
|
78
|
-
|
|
96
|
+
async function ask(channel, text, pubkey) {
|
|
97
|
+
// Built per turn, for the person whose turn it is — and kept current for as
|
|
98
|
+
// long as that turn runs.
|
|
99
|
+
const client = clientFor(pubkey);
|
|
79
100
|
const existing = sessions.get(channel);
|
|
80
101
|
if (existing) {
|
|
81
102
|
try {
|
|
@@ -212,7 +233,7 @@ export function buzzBridge(options) {
|
|
|
212
233
|
void relay.react(event.id, SEEN);
|
|
213
234
|
const stopTyping = relay.typingIn(channel);
|
|
214
235
|
try {
|
|
215
|
-
const reply = await serialize(channel, () => ask(channel, text,
|
|
236
|
+
const reply = await serialize(channel, () => ask(channel, text, event.pubkey));
|
|
216
237
|
if (!reply) {
|
|
217
238
|
/**
|
|
218
239
|
* Silence is the worst answer available.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SpeakerResolution } from "@kybernesis/enterprise";
|
|
2
|
+
/** What the eve client needs to authenticate one person's turn. */
|
|
3
|
+
export interface SpeakerCredentials {
|
|
4
|
+
/** Bearer resolver — called by the client before every HTTP request. */
|
|
5
|
+
bearer: () => Promise<string>;
|
|
6
|
+
/** Header resolver, for the same reason and on the same schedule. */
|
|
7
|
+
headers: () => Promise<Readonly<Record<string, string>>>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Credentials for a turn, resolved per request rather than captured once.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* An identity token lives about five minutes. That was invisible while turns
|
|
14
|
+
* ended early, and became a failure the moment they ran to completion: a turn
|
|
15
|
+
* doing six minutes of real work reconnected its stream with the token it began
|
|
16
|
+
* with, the agent answered "Authorization is required for this route", and the
|
|
17
|
+
* reply was lost after every tool call had already succeeded. In a channel that
|
|
18
|
+
* reads as the agent typing for five minutes and then saying nothing at all.
|
|
19
|
+
*
|
|
20
|
+
* Resolving per request costs nothing in the normal case, because the resolver
|
|
21
|
+
* caches and re-mints only near expiry. It also preserves the property the
|
|
22
|
+
* short lifetime exists for: access withdrawn mid-turn stops the next request
|
|
23
|
+
* rather than being noticed after the work is done.
|
|
24
|
+
*/
|
|
25
|
+
export declare function speakerCredentials(resolve: (externalId: string) => Promise<SpeakerResolution>): (externalId: string) => SpeakerCredentials;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials for a turn, resolved per request rather than captured once.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* An identity token lives about five minutes. That was invisible while turns
|
|
6
|
+
* ended early, and became a failure the moment they ran to completion: a turn
|
|
7
|
+
* doing six minutes of real work reconnected its stream with the token it began
|
|
8
|
+
* with, the agent answered "Authorization is required for this route", and the
|
|
9
|
+
* reply was lost after every tool call had already succeeded. In a channel that
|
|
10
|
+
* reads as the agent typing for five minutes and then saying nothing at all.
|
|
11
|
+
*
|
|
12
|
+
* Resolving per request costs nothing in the normal case, because the resolver
|
|
13
|
+
* caches and re-mints only near expiry. It also preserves the property the
|
|
14
|
+
* short lifetime exists for: access withdrawn mid-turn stops the next request
|
|
15
|
+
* rather than being noticed after the work is done.
|
|
16
|
+
*/
|
|
17
|
+
export function speakerCredentials(resolve) {
|
|
18
|
+
return (externalId) => {
|
|
19
|
+
const current = async () => {
|
|
20
|
+
const speaker = await resolve(externalId);
|
|
21
|
+
if (!speaker.linked) {
|
|
22
|
+
// Failing here is the point: the alternative is finishing a turn with a
|
|
23
|
+
// credential the control plane has already withdrawn.
|
|
24
|
+
throw new Error(`no longer authorized (${speaker.reason})`);
|
|
25
|
+
}
|
|
26
|
+
return { token: speaker.token, bundle: speaker.bundle };
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
bearer: async () => (await current()).token,
|
|
30
|
+
headers: async () => {
|
|
31
|
+
const { bundle } = await current();
|
|
32
|
+
return bundle ? { "x-kybernesis-bundle": bundle } : {};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/buzz",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Put an eve agent in a Buzz workspace as a member: per-speaker verified identity resolved through the control plane, self-service identity linking, presence, typing and seen signals.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|