@yolo-labs/yolobridge 0.2.0 → 0.8.0
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/api-client.js +88 -3
- package/dist/attach-cmd.js +535 -44
- package/dist/cli.js +70 -5
- package/dist/config-store.js +58 -4
- package/dist/connection-state.js +108 -0
- package/dist/detach-cmd.js +42 -4
- package/dist/mcp-proxy.js +3 -2
- package/dist/status-cmd.js +30 -0
- package/package.json +1 -1
package/dist/api-client.js
CHANGED
|
@@ -29,12 +29,40 @@ async function parseErrorBody(res) {
|
|
|
29
29
|
return { message: `HTTP ${res.status}` };
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* `POST /v1/workspaces/:workspaceId/yolobridge/attach`.
|
|
34
|
+
*
|
|
35
|
+
* `scopedToken` / `scopedTokenExpiresAt` are the workspace-scoped daemon
|
|
36
|
+
* credential common-api mints at attach
|
|
37
|
+
* (`docs/YOLOBRIDGE_SCOPED_CREDENTIAL_PLAN.md`): a token confined to THIS
|
|
38
|
+
* workspace's YoloBridge surface, which the post-attach calls
|
|
39
|
+
* (`detach`/`openStream`/`postHeartbeat`/`postReadOutputReply`) use instead of
|
|
40
|
+
* the full-account token. `scopedTokenExpiresAt` is absolute epoch-ms computed
|
|
41
|
+
* server-side at mint, so the refresh schedule never requires decoding the JWT.
|
|
42
|
+
*
|
|
43
|
+
* REQUIRED since 0.7.0 (card 09, D6 — "no backwards support"). Both fields or
|
|
44
|
+
* neither is still the rule; what changed is that "neither" is now an ERROR
|
|
45
|
+
* rather than a degrade. This used to be optional to protect a daemon binary
|
|
46
|
+
* frozen on a laptop against a common-api predating the mint — but Boundary B
|
|
47
|
+
* now refuses an account token on every post-attach route, so a daemon that
|
|
48
|
+
* attaches without a scoped credential cannot do anything afterwards. Accepting
|
|
49
|
+
* the response would buy it exactly one successful call and then a 403 loop
|
|
50
|
+
* with no diagnosis; failing here names the real problem at the one moment the
|
|
51
|
+
* operator is still watching the terminal.
|
|
52
|
+
*
|
|
53
|
+
* Every other exported function's signature is unchanged: they still take "the
|
|
54
|
+
* bearer token to send" via `ApiClientConfig`, and which token that is remains
|
|
55
|
+
* the caller's decision.
|
|
56
|
+
*/
|
|
57
|
+
export async function attach(cfg, workspaceId, hostLabel, remoteHost) {
|
|
33
58
|
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
34
59
|
const res = await fetchImpl(`${base(cfg)}/v1/workspaces/${workspaceId}/yolobridge/attach`, {
|
|
35
60
|
method: 'POST',
|
|
36
61
|
headers: { ...authHeaders(cfg), 'Content-Type': 'application/json' },
|
|
37
|
-
body: JSON.stringify(
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
...(hostLabel ? { hostLabel } : {}),
|
|
64
|
+
...(remoteHost && Object.values(remoteHost).some(Boolean) ? { remoteHost } : {}),
|
|
65
|
+
}),
|
|
38
66
|
});
|
|
39
67
|
if (!res.ok) {
|
|
40
68
|
const { message, code } = await parseErrorBody(res);
|
|
@@ -44,7 +72,54 @@ export async function attach(cfg, workspaceId, hostLabel) {
|
|
|
44
72
|
if (typeof body?.tileId !== 'string' || typeof body?.attachmentId !== 'string') {
|
|
45
73
|
throw new YoloBridgeApiError('attach returned an unexpected shape', res.status);
|
|
46
74
|
}
|
|
47
|
-
|
|
75
|
+
// Both-or-neither, and "neither" is a failure (see the doc comment). A token
|
|
76
|
+
// with no expiry cannot be renewed on time and an expiry with no token is
|
|
77
|
+
// nothing, so a half-pair is refused by the same check — there is no shape
|
|
78
|
+
// here that yields a usable-but-unschedulable credential.
|
|
79
|
+
if (typeof body?.scopedToken !== 'string' || typeof body?.scopedTokenExpiresAt !== 'number') {
|
|
80
|
+
throw new YoloBridgeApiError('attach returned no workspace-scoped credential — this server cannot host a YoloBridge daemon', res.status);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
tileId: body.tileId,
|
|
84
|
+
attachmentId: body.attachmentId,
|
|
85
|
+
scopedToken: body.scopedToken,
|
|
86
|
+
scopedTokenExpiresAt: body.scopedTokenExpiresAt,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* `POST /v1/workspaces/:workspaceId/yolobridge/attach/:attachmentId/refresh` —
|
|
91
|
+
* renew the workspace-scoped daemon credential (card 07,
|
|
92
|
+
* docs/YOLOBRIDGE_SCOPED_CREDENTIAL_PLAN.md, D1).
|
|
93
|
+
*
|
|
94
|
+
* `cfg.accessToken` MUST be the scoped token being renewed: this endpoint
|
|
95
|
+
* authenticates by the presented credential itself ("proof of recent prior
|
|
96
|
+
* possession"), so the token IS the request's identity. There is no refresh
|
|
97
|
+
* credential — deliberately. The daemon never holds a long-lived one, which is
|
|
98
|
+
* the entire point of the scoping work: a stolen laptop yields a credential
|
|
99
|
+
* that expires in an hour and can only be renewed while it is still fresh.
|
|
100
|
+
*
|
|
101
|
+
* The server accepts a token that has JUST expired, within a narrow grace
|
|
102
|
+
* window (15 minutes server-side), so a clock skew or a short sleep across the
|
|
103
|
+
* scheduled renewal recovers instead of forcing a re-attach. Past that, the
|
|
104
|
+
* refusal is terminal and the remedy is `yolo-bridge attach`.
|
|
105
|
+
*
|
|
106
|
+
* Unlike `attach`, the response shape is validated STRICTLY: this call only
|
|
107
|
+
* ever reaches a server that already issued a scoped token, so a reply missing
|
|
108
|
+
* one is a genuine protocol disagreement, not an old-server degrade. Returning
|
|
109
|
+
* a half-pair would leave the caller unable to schedule the next renewal.
|
|
110
|
+
*/
|
|
111
|
+
export async function refreshScopedToken(cfg, workspaceId, attachmentId) {
|
|
112
|
+
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
113
|
+
const res = await fetchImpl(`${base(cfg)}/v1/workspaces/${workspaceId}/yolobridge/attach/${encodeURIComponent(attachmentId)}/refresh`, { method: 'POST', headers: authHeaders(cfg) });
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
const { message, code } = await parseErrorBody(res);
|
|
116
|
+
throw new YoloBridgeApiError(`scoped credential refresh failed: ${message}`, res.status, code);
|
|
117
|
+
}
|
|
118
|
+
const body = (await res.json());
|
|
119
|
+
if (typeof body?.scopedToken !== 'string' || typeof body?.scopedTokenExpiresAt !== 'number') {
|
|
120
|
+
throw new YoloBridgeApiError('scoped credential refresh returned an unexpected shape', res.status);
|
|
121
|
+
}
|
|
122
|
+
return { scopedToken: body.scopedToken, scopedTokenExpiresAt: body.scopedTokenExpiresAt };
|
|
48
123
|
}
|
|
49
124
|
/**
|
|
50
125
|
* `GET /v1/workspaces/selectable` — slim `{id,name,status}` list of the
|
|
@@ -78,6 +153,16 @@ export async function listSelectableWorkspaces(cfg) {
|
|
|
78
153
|
}
|
|
79
154
|
return workspaces;
|
|
80
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* `DELETE /v1/workspaces/:workspaceId/yolobridge/attach/:attachmentId`.
|
|
158
|
+
*
|
|
159
|
+
* `cfg.accessToken` must be the WORKSPACE-SCOPED credential, not the account
|
|
160
|
+
* token: this is one of the daemon-only routes Boundary B guards, and an
|
|
161
|
+
* account token is refused there with 403 YOLOBRIDGE_SCOPED_TOKEN_REQUIRED
|
|
162
|
+
* (card 09). Both callers comply — the daemon's own cleanup path via
|
|
163
|
+
* `scopedCfg()`, and standalone `yolo-bridge detach` via the credential
|
|
164
|
+
* `attachment.json` persisted at attach.
|
|
165
|
+
*/
|
|
81
166
|
export async function detach(cfg, workspaceId, attachmentId) {
|
|
82
167
|
const fetchImpl = cfg.fetchImpl ?? fetch;
|
|
83
168
|
const res = await fetchImpl(`${base(cfg)}/v1/workspaces/${workspaceId}/yolobridge/attach/${attachmentId}`, {
|