@agent-native/core 0.175.0 → 0.175.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/corpus/README.md +1 -1
- package/corpus/templates/assets/app/root.tsx +5 -2
- package/corpus/templates/clips/actions/create-recording.ts +8 -1
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +3 -1
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +11 -0
- package/corpus/templates/clips/app/components/player/timestamped-comment-button.tsx +2 -41
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +48 -4
- package/corpus/templates/clips/app/i18n/en-US.ts +1 -2
- package/corpus/templates/clips/app/lib/fmp4-seek.ts +274 -0
- package/corpus/templates/clips/app/lib/fmp4.ts +169 -0
- package/corpus/templates/clips/app/lib/mse-video-loader.ts +351 -109
- package/corpus/templates/clips/app/root.tsx +1 -1
- package/corpus/templates/clips/changelog/2026-08-25-seeking-into-a-long-recording-no-longer-sticks-on-buffering.md +6 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +103 -13
- package/corpus/templates/clips/server/lib/resumable-upload-provider.ts +2 -2
- package/corpus/templates/clips/server/lib/video-storage.ts +2 -2
- package/corpus/templates/forms/app/components/builder/FieldRenderer.tsx +162 -157
- package/corpus/templates/tasks/app/root.tsx +5 -2
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/file-upload/builder.d.ts +2 -2
- package/dist/file-upload/builder.js +28 -26
- package/dist/file-upload/registry.js +11 -8
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/builder-api-auth.d.ts +31 -0
- package/dist/server/builder-api-auth.js +62 -0
- package/dist/server/builder-oauth.d.ts +5 -3
- package/dist/server/builder-oauth.js +40 -12
- package/dist/server/core-routes-plugin.js +5 -2
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +2 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +3 -3
|
@@ -36,13 +36,13 @@ async function assertOk(res, label) {
|
|
|
36
36
|
throw new Error(`${label} (${res.status}): ${body || res.statusText}`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
async function uploadLargeFileViaSignedUrl(input,
|
|
39
|
+
async function uploadLargeFileViaSignedUrl(input, authorization, bareMimeType, bytes) {
|
|
40
40
|
const name = input.filename ?? "upload";
|
|
41
41
|
const mb = (bytes.byteLength / (1024 * 1024)).toFixed(1);
|
|
42
42
|
console.log(`[builder-upload] large-file path: ${name} ${mb}MB ${bareMimeType}`);
|
|
43
43
|
// Step 1 — request a signed URL.
|
|
44
44
|
console.log(`[builder-upload] step 1: requesting signed URL`);
|
|
45
|
-
const { uploadUrl, assetId, requiredHeaders } = await requestBuilderSignedUrl(
|
|
45
|
+
const { uploadUrl, assetId, requiredHeaders } = await requestBuilderSignedUrl(authorization, name, bareMimeType, bytes.byteLength);
|
|
46
46
|
console.log(`[builder-upload] step 1 ok: assetId=${assetId}`);
|
|
47
47
|
// Step 2 — PUT bytes directly to GCS. Only requiredHeaders; no Authorization
|
|
48
48
|
// (signed URL carries its own auth — extra signed headers break the signature).
|
|
@@ -56,20 +56,20 @@ async function uploadLargeFileViaSignedUrl(input, privateKey, bareMimeType, byte
|
|
|
56
56
|
console.log(`[builder-upload] step 2 ok [${assetId}]: GCS ${step2Res.status} etag=${step2Res.headers.get("etag") ?? "none"}`);
|
|
57
57
|
// Step 3 — register the asset and get the CDN URL.
|
|
58
58
|
console.log(`[builder-upload] step 3: registering asset - ${assetId}, ${input.filename}`);
|
|
59
|
-
const { url, id } = await completeBuilderUpload(
|
|
59
|
+
const { url, id } = await completeBuilderUpload(authorization, assetId, input.filename, {
|
|
60
60
|
stableUrl: input.stableUrl,
|
|
61
61
|
recordAsset: input.recordAsset,
|
|
62
62
|
});
|
|
63
63
|
console.log(`[builder-upload] done [${assetId}]: ${url}`);
|
|
64
64
|
return { url, id, provider: "builder" };
|
|
65
65
|
}
|
|
66
|
-
async function requestBuilderSignedUrl(
|
|
66
|
+
async function requestBuilderSignedUrl(authorization, filename, mimeType, size, resumable = false) {
|
|
67
67
|
const host = builderUploadHost();
|
|
68
68
|
const url = new URL("/api/v1/upload/signed-url", host);
|
|
69
69
|
const res = await fetchWithTimeout(url.toString(), {
|
|
70
70
|
method: "POST",
|
|
71
71
|
headers: {
|
|
72
|
-
Authorization:
|
|
72
|
+
Authorization: authorization,
|
|
73
73
|
"Content-Type": "application/json",
|
|
74
74
|
},
|
|
75
75
|
body: JSON.stringify({
|
|
@@ -90,7 +90,7 @@ async function requestBuilderSignedUrl(privateKey, filename, mimeType, size, res
|
|
|
90
90
|
requiredHeaders: json.requiredHeaders,
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
-
async function completeBuilderUpload(
|
|
93
|
+
async function completeBuilderUpload(authorization, assetId, filename, options) {
|
|
94
94
|
const host = builderUploadHost();
|
|
95
95
|
const url = new URL("/api/v1/upload/complete", host);
|
|
96
96
|
if (options?.stableUrl) {
|
|
@@ -100,7 +100,7 @@ async function completeBuilderUpload(privateKey, assetId, filename, options) {
|
|
|
100
100
|
const res = await fetchWithTimeout(url.toString(), {
|
|
101
101
|
method: "POST",
|
|
102
102
|
headers: {
|
|
103
|
-
Authorization:
|
|
103
|
+
Authorization: authorization,
|
|
104
104
|
"Content-Type": "application/json",
|
|
105
105
|
},
|
|
106
106
|
body: JSON.stringify({
|
|
@@ -144,10 +144,22 @@ async function uploadSmallFile(url, init) {
|
|
|
144
144
|
const statusText = response?.statusText ?? "no response";
|
|
145
145
|
throw new Error(`Builder.io upload failed (${status}): ${lastErrorBody || statusText}`);
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Builder gates every `/api/v1/upload/*` endpoint on `builder:assets:write`.
|
|
149
|
+
* Legacy `bpk-` keys skip that check, which is why uploads kept working for
|
|
150
|
+
* older connections while OAuth-only ones could not upload at all.
|
|
151
|
+
*/
|
|
152
|
+
async function assetAuthorization() {
|
|
153
|
+
const [{ resolveBuilderApiAuthorization }, { BUILDER_ASSETS_WRITE_SCOPE }] = await Promise.all([
|
|
154
|
+
import("../server/builder-api-auth.js"),
|
|
155
|
+
import("../server/builder-oauth.js"),
|
|
156
|
+
]);
|
|
157
|
+
return resolveBuilderApiAuthorization(BUILDER_ASSETS_WRITE_SCOPE);
|
|
158
|
+
}
|
|
147
159
|
/**
|
|
148
160
|
* Built-in Builder.io file upload provider.
|
|
149
|
-
* Uses the same
|
|
150
|
-
*
|
|
161
|
+
* Uses the same Builder connection as the browser/background-agent flows, so
|
|
162
|
+
* connecting Builder once (via the sidebar "Connect Builder" action)
|
|
151
163
|
* automatically enables file uploads.
|
|
152
164
|
*
|
|
153
165
|
* Upload API: https://www.builder.io/c/docs/upload-api
|
|
@@ -158,11 +170,7 @@ export const builderFileUploadProvider = {
|
|
|
158
170
|
isConfigured: () => !!process.env.BUILDER_PRIVATE_KEY,
|
|
159
171
|
upload: async (input) => {
|
|
160
172
|
const { data, filename, mimeType } = input;
|
|
161
|
-
const
|
|
162
|
-
const privateKey = await resolveBuilderPrivateKey();
|
|
163
|
-
if (!privateKey) {
|
|
164
|
-
throw new Error("BUILDER_PRIVATE_KEY is not set");
|
|
165
|
-
}
|
|
173
|
+
const authorization = await assetAuthorization();
|
|
166
174
|
// Strip any media-type parameters (e.g. `;codecs=avc1,opus` from
|
|
167
175
|
// MediaRecorder blobs) — Builder's upload API parses the body as raw
|
|
168
176
|
// binary only when Content-Type is a bare MIME type. A parameterized
|
|
@@ -175,7 +183,7 @@ export const builderFileUploadProvider = {
|
|
|
175
183
|
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
176
184
|
const mb = (bytes.byteLength / (1024 * 1024)).toFixed(1);
|
|
177
185
|
if (shouldUseSignedUrlUpload(bytes, bareMimeType)) {
|
|
178
|
-
return uploadLargeFileViaSignedUrl(input,
|
|
186
|
+
return uploadLargeFileViaSignedUrl(input, authorization, bareMimeType, bytes);
|
|
179
187
|
}
|
|
180
188
|
console.log(`[builder-upload] small-file path: ${filename ?? "upload"} ${mb}MB ${bareMimeType}`);
|
|
181
189
|
const url = new URL("/api/v1/upload", builderUploadHost());
|
|
@@ -188,7 +196,7 @@ export const builderFileUploadProvider = {
|
|
|
188
196
|
const response = await uploadSmallFile(url, {
|
|
189
197
|
method: "POST",
|
|
190
198
|
headers: {
|
|
191
|
-
Authorization:
|
|
199
|
+
Authorization: authorization,
|
|
192
200
|
"Content-Type": bareMimeType,
|
|
193
201
|
},
|
|
194
202
|
body: makeBody(bytes, bareMimeType),
|
|
@@ -225,12 +233,9 @@ export const builderFileUploadProvider = {
|
|
|
225
233
|
},
|
|
226
234
|
resumable: {
|
|
227
235
|
async startSession(filename, mimeType, maxBytes) {
|
|
228
|
-
const
|
|
229
|
-
const privateKey = await resolveBuilderPrivateKey();
|
|
230
|
-
if (!privateKey)
|
|
231
|
-
throw new Error("BUILDER_PRIVATE_KEY is not set");
|
|
236
|
+
const authorization = await assetAuthorization();
|
|
232
237
|
console.log(`[builder-resumable] starting session: ${filename} ${mimeType} ${maxBytes} bytes`);
|
|
233
|
-
const { uploadUrl, assetId, requiredHeaders } = await requestBuilderSignedUrl(
|
|
238
|
+
const { uploadUrl, assetId, requiredHeaders } = await requestBuilderSignedUrl(authorization, filename, mimeType, maxBytes, true);
|
|
234
239
|
console.log(`[builder-resumable] session step 1 ok: assetId=${assetId}`);
|
|
235
240
|
const initHeaders = {
|
|
236
241
|
"Content-Type": mimeType,
|
|
@@ -305,13 +310,10 @@ export const builderFileUploadProvider = {
|
|
|
305
310
|
: new Error("GCS PUT failed after retries");
|
|
306
311
|
},
|
|
307
312
|
async completeSession(session, filename, options) {
|
|
308
|
-
const
|
|
309
|
-
const privateKey = await resolveBuilderPrivateKey();
|
|
310
|
-
if (!privateKey)
|
|
311
|
-
throw new Error("BUILDER_PRIVATE_KEY is not set");
|
|
313
|
+
const authorization = await assetAuthorization();
|
|
312
314
|
const assetId = session.meta.assetId;
|
|
313
315
|
console.log(`[builder-resumable] completing upload: assetId=${assetId}`);
|
|
314
|
-
const { url } = await completeBuilderUpload(
|
|
316
|
+
const { url } = await completeBuilderUpload(authorization, assetId, filename, {
|
|
315
317
|
stableUrl: options?.stableUrl || session.meta.stableUrl === true,
|
|
316
318
|
recordAsset: options?.recordAsset ??
|
|
317
319
|
(session.meta.recordAsset === false ? false : undefined),
|
|
@@ -44,8 +44,10 @@ export async function getActiveFileUploadProviderForRequest() {
|
|
|
44
44
|
return builderFileUploadProvider;
|
|
45
45
|
}
|
|
46
46
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Either Builder credential kind counts. Checking only the private key
|
|
48
|
+
// reported every OAuth-only connection as having no upload provider.
|
|
49
|
+
const { hasBuilderApiCredentialCustody } = await import("../server/builder-api-auth.js");
|
|
50
|
+
if (await hasBuilderApiCredentialCustody()) {
|
|
49
51
|
return builderFileUploadProvider;
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -79,17 +81,18 @@ export async function uploadFile(input) {
|
|
|
79
81
|
// via runWithRequestContext — actions always have one via action-routes.ts).
|
|
80
82
|
// Two separate try-catch blocks ensure a real upload failure is never
|
|
81
83
|
// silently swallowed as a "no credentials" case.
|
|
82
|
-
let
|
|
84
|
+
let hasBuilderCredential = false;
|
|
83
85
|
try {
|
|
84
|
-
const {
|
|
85
|
-
|
|
86
|
+
const { hasBuilderApiCredentialCustody } = await import("../server/builder-api-auth.js");
|
|
87
|
+
hasBuilderCredential = await hasBuilderApiCredentialCustody();
|
|
86
88
|
}
|
|
87
89
|
catch (err) {
|
|
88
|
-
// DB unavailable or credential store not ready — can't resolve
|
|
89
|
-
// Return an unavailable-provider state below; never fall back
|
|
90
|
+
// DB unavailable or credential store not ready — can't resolve a
|
|
91
|
+
// credential. Return an unavailable-provider state below; never fall back
|
|
92
|
+
// to SQL.
|
|
90
93
|
console.warn("[agent-native] Builder credential check failed:", err instanceof Error ? err.message : String(err));
|
|
91
94
|
}
|
|
92
|
-
if (
|
|
95
|
+
if (hasBuilderCredential) {
|
|
93
96
|
// Credentials confirmed — attempt the upload. Real errors (network,
|
|
94
97
|
// API, rate-limit) propagate to the caller; do NOT catch them here.
|
|
95
98
|
return await builderFileUploadProvider.upload(input);
|
|
@@ -137,13 +137,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
137
137
|
inputSchema: {
|
|
138
138
|
type: string;
|
|
139
139
|
properties: {
|
|
140
|
-
count?: undefined;
|
|
141
140
|
query?: undefined;
|
|
142
141
|
minutes?: undefined;
|
|
143
142
|
limit?: undefined;
|
|
144
143
|
clientHint?: undefined;
|
|
145
144
|
timestamp?: undefined;
|
|
146
145
|
chapterId?: undefined;
|
|
146
|
+
count?: undefined;
|
|
147
147
|
startAt?: undefined;
|
|
148
148
|
endAt?: undefined;
|
|
149
149
|
reason?: undefined;
|
|
@@ -159,7 +159,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
159
159
|
inputSchema: {
|
|
160
160
|
type: string;
|
|
161
161
|
properties: {
|
|
162
|
-
count?: undefined;
|
|
163
162
|
query: {
|
|
164
163
|
type: string;
|
|
165
164
|
description: string;
|
|
@@ -175,6 +174,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
175
174
|
clientHint?: undefined;
|
|
176
175
|
timestamp?: undefined;
|
|
177
176
|
chapterId?: undefined;
|
|
177
|
+
count?: undefined;
|
|
178
178
|
startAt?: undefined;
|
|
179
179
|
endAt?: undefined;
|
|
180
180
|
reason?: undefined;
|
|
@@ -190,7 +190,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
190
190
|
inputSchema: {
|
|
191
191
|
type: string;
|
|
192
192
|
properties: {
|
|
193
|
-
count?: undefined;
|
|
194
193
|
minutes: {
|
|
195
194
|
type: string;
|
|
196
195
|
description: string;
|
|
@@ -200,6 +199,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
200
199
|
clientHint?: undefined;
|
|
201
200
|
timestamp?: undefined;
|
|
202
201
|
chapterId?: undefined;
|
|
202
|
+
count?: undefined;
|
|
203
203
|
startAt?: undefined;
|
|
204
204
|
endAt?: undefined;
|
|
205
205
|
reason?: undefined;
|
|
@@ -216,7 +216,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
216
216
|
type: string;
|
|
217
217
|
required: string[];
|
|
218
218
|
properties: {
|
|
219
|
-
count?: undefined;
|
|
220
219
|
query: {
|
|
221
220
|
type: string;
|
|
222
221
|
description: string;
|
|
@@ -235,6 +234,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
235
234
|
};
|
|
236
235
|
timestamp?: undefined;
|
|
237
236
|
chapterId?: undefined;
|
|
237
|
+
count?: undefined;
|
|
238
238
|
startAt?: undefined;
|
|
239
239
|
endAt?: undefined;
|
|
240
240
|
reason?: undefined;
|
|
@@ -250,7 +250,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
250
250
|
type: string;
|
|
251
251
|
required: string[];
|
|
252
252
|
properties: {
|
|
253
|
-
count?: undefined;
|
|
254
253
|
query?: undefined;
|
|
255
254
|
minutes?: undefined;
|
|
256
255
|
limit?: undefined;
|
|
@@ -264,6 +263,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
264
263
|
description: string;
|
|
265
264
|
};
|
|
266
265
|
chapterId?: undefined;
|
|
266
|
+
count?: undefined;
|
|
267
267
|
startAt?: undefined;
|
|
268
268
|
endAt?: undefined;
|
|
269
269
|
includeMicrophone?: undefined;
|
|
@@ -314,13 +314,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
314
314
|
type: string;
|
|
315
315
|
required: string[];
|
|
316
316
|
properties: {
|
|
317
|
-
count?: undefined;
|
|
318
317
|
query?: undefined;
|
|
319
318
|
minutes?: undefined;
|
|
320
319
|
limit?: undefined;
|
|
321
320
|
clientHint?: undefined;
|
|
322
321
|
timestamp?: undefined;
|
|
323
322
|
chapterId?: undefined;
|
|
323
|
+
count?: undefined;
|
|
324
324
|
startAt: {
|
|
325
325
|
type: string;
|
|
326
326
|
description: string;
|
|
@@ -349,13 +349,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
|
|
|
349
349
|
type: string;
|
|
350
350
|
required: string[];
|
|
351
351
|
properties: {
|
|
352
|
-
count?: undefined;
|
|
353
352
|
query?: undefined;
|
|
354
353
|
minutes?: undefined;
|
|
355
354
|
limit?: undefined;
|
|
356
355
|
clientHint?: undefined;
|
|
357
356
|
timestamp?: undefined;
|
|
358
357
|
chapterId?: undefined;
|
|
358
|
+
count?: undefined;
|
|
359
359
|
startAt?: undefined;
|
|
360
360
|
endAt?: undefined;
|
|
361
361
|
reason?: undefined;
|
|
@@ -16,18 +16,18 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
|
|
|
16
16
|
error?: undefined;
|
|
17
17
|
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
|
-
count?: undefined;
|
|
20
19
|
updated: number;
|
|
21
20
|
error?: undefined;
|
|
22
21
|
ok?: undefined;
|
|
23
|
-
} | {
|
|
24
22
|
count?: undefined;
|
|
23
|
+
} | {
|
|
25
24
|
updated?: undefined;
|
|
26
25
|
error: string;
|
|
27
26
|
ok?: undefined;
|
|
28
|
-
} | {
|
|
29
27
|
count?: undefined;
|
|
28
|
+
} | {
|
|
30
29
|
updated?: undefined;
|
|
31
30
|
error?: undefined;
|
|
32
31
|
ok: boolean;
|
|
32
|
+
count?: undefined;
|
|
33
33
|
}>>;
|
|
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
-
error?: undefined;
|
|
45
|
-
ok?: undefined;
|
|
46
44
|
summary: import("./types.js").TraceSummary;
|
|
47
45
|
spans: import("./types.js").TraceSpan[];
|
|
48
46
|
id?: undefined;
|
|
49
|
-
} | {
|
|
50
47
|
error?: undefined;
|
|
51
48
|
ok?: undefined;
|
|
49
|
+
} | {
|
|
52
50
|
summary?: undefined;
|
|
53
51
|
spans?: undefined;
|
|
54
52
|
id: string;
|
|
55
|
-
|
|
53
|
+
error?: undefined;
|
|
56
54
|
ok?: undefined;
|
|
55
|
+
} | {
|
|
57
56
|
summary?: undefined;
|
|
58
57
|
spans?: undefined;
|
|
59
58
|
id?: undefined;
|
|
60
59
|
error: any;
|
|
60
|
+
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
-
error?: undefined;
|
|
63
62
|
summary?: undefined;
|
|
64
63
|
spans?: undefined;
|
|
65
64
|
id?: undefined;
|
|
65
|
+
error?: undefined;
|
|
66
66
|
ok: boolean;
|
|
67
67
|
}>>;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
|
|
|
34
34
|
/** POST /_agent-native/secrets/:key — write a secret. */
|
|
35
35
|
export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
36
36
|
error: string;
|
|
37
|
-
ok?: undefined;
|
|
38
37
|
status?: undefined;
|
|
38
|
+
ok?: undefined;
|
|
39
39
|
} | {
|
|
40
|
-
error?: undefined;
|
|
41
40
|
ok: boolean;
|
|
42
41
|
status: string;
|
|
42
|
+
error?: undefined;
|
|
43
43
|
} | {
|
|
44
|
-
ok?: undefined;
|
|
45
44
|
error: string;
|
|
46
45
|
removed?: undefined;
|
|
46
|
+
ok?: undefined;
|
|
47
47
|
} | {
|
|
48
|
-
error?: undefined;
|
|
49
48
|
ok: boolean;
|
|
50
49
|
removed: boolean;
|
|
50
|
+
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — validate an optional candidate value
|
|
54
54
|
* or the current stored value without changing anything.
|
|
55
55
|
*/
|
|
56
56
|
export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
57
|
-
ok?: undefined;
|
|
58
57
|
error: string;
|
|
59
58
|
note?: undefined;
|
|
59
|
+
ok?: undefined;
|
|
60
60
|
} | {
|
|
61
|
-
error?: undefined;
|
|
62
61
|
ok: boolean;
|
|
63
62
|
note?: undefined;
|
|
64
|
-
} | {
|
|
65
63
|
error?: undefined;
|
|
64
|
+
} | {
|
|
66
65
|
ok: boolean;
|
|
67
66
|
note: string;
|
|
67
|
+
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
-
error?: undefined;
|
|
99
98
|
ok: boolean;
|
|
100
99
|
key: string;
|
|
101
|
-
} | {
|
|
102
100
|
error?: undefined;
|
|
101
|
+
} | {
|
|
103
102
|
ok: boolean;
|
|
104
103
|
removed: boolean;
|
|
104
|
+
error?: undefined;
|
|
105
105
|
}>>;
|
|
@@ -34,10 +34,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
34
34
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
35
35
|
error: any;
|
|
36
36
|
} | {
|
|
37
|
-
error?: undefined;
|
|
38
37
|
ok: boolean;
|
|
39
38
|
key: string;
|
|
40
39
|
baseUrlKey?: string;
|
|
41
40
|
scope: AgentEngineApiKeyScope;
|
|
41
|
+
error?: undefined;
|
|
42
42
|
}>>;
|
|
43
43
|
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization for Builder.io REST asset calls (`/api/v1/upload/*`).
|
|
3
|
+
*
|
|
4
|
+
* Two credential kinds reach those endpoints and they are not interchangeable:
|
|
5
|
+
* a legacy `bpk-` private key bypasses Builder's OAuth scope middleware
|
|
6
|
+
* entirely, while an OAuth access token is checked against the scopes its
|
|
7
|
+
* grant was issued with. New Builder connections store only an OAuth grant, so
|
|
8
|
+
* a caller that knows about private keys alone cannot upload for them at all.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the `Authorization` header for a Builder asset API call.
|
|
12
|
+
*
|
|
13
|
+
* OAuth wins whenever the request's owner has a grant. An unusable grant
|
|
14
|
+
* throws rather than falling back to a private key: falling back would let a
|
|
15
|
+
* deploy-level or stale `bpk-` key act for a user who never authorized it.
|
|
16
|
+
*
|
|
17
|
+
* @param requiredScope OAuth scope the endpoint enforces. Omit for endpoints
|
|
18
|
+
* with no scope gate.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveBuilderApiAuthorization(requiredScope?: string): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* Whether Builder.io can authenticate an asset call for this request — an
|
|
23
|
+
* OAuth grant, or a legacy private key.
|
|
24
|
+
*
|
|
25
|
+
* Storage gates and provider selection use this. It intentionally does not
|
|
26
|
+
* verify the grant is usable: answering `false` for a connected user whose
|
|
27
|
+
* grant needs re-authorizing would report storage as unconfigured and send
|
|
28
|
+
* them to set up something they already have, instead of letting the upload
|
|
29
|
+
* path say what is actually wrong.
|
|
30
|
+
*/
|
|
31
|
+
export declare function hasBuilderApiCredentialCustody(): Promise<boolean>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization for Builder.io REST asset calls (`/api/v1/upload/*`).
|
|
3
|
+
*
|
|
4
|
+
* Two credential kinds reach those endpoints and they are not interchangeable:
|
|
5
|
+
* a legacy `bpk-` private key bypasses Builder's OAuth scope middleware
|
|
6
|
+
* entirely, while an OAuth access token is checked against the scopes its
|
|
7
|
+
* grant was issued with. New Builder connections store only an OAuth grant, so
|
|
8
|
+
* a caller that knows about private keys alone cannot upload for them at all.
|
|
9
|
+
*/
|
|
10
|
+
import { getBuilderOAuthSession, hasBuilderOAuthSession, } from "./builder-oauth.js";
|
|
11
|
+
import { resolveBuilderPrivateKey } from "./credential-provider.js";
|
|
12
|
+
import { getRequestOrgId, getRequestUserEmail } from "./request-context.js";
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the `Authorization` header for a Builder asset API call.
|
|
15
|
+
*
|
|
16
|
+
* OAuth wins whenever the request's owner has a grant. An unusable grant
|
|
17
|
+
* throws rather than falling back to a private key: falling back would let a
|
|
18
|
+
* deploy-level or stale `bpk-` key act for a user who never authorized it.
|
|
19
|
+
*
|
|
20
|
+
* @param requiredScope OAuth scope the endpoint enforces. Omit for endpoints
|
|
21
|
+
* with no scope gate.
|
|
22
|
+
*/
|
|
23
|
+
export async function resolveBuilderApiAuthorization(requiredScope) {
|
|
24
|
+
const ownerEmail = getRequestUserEmail();
|
|
25
|
+
// Bind to the request's organization, not the user's currently active one:
|
|
26
|
+
// the grant is org-scoped, and a recording that finalizes after the user
|
|
27
|
+
// switched org must still authorize against the org it belongs to. The
|
|
28
|
+
// legacy private-key path already resolves this way.
|
|
29
|
+
const orgId = getRequestOrgId();
|
|
30
|
+
if (ownerEmail && (await hasBuilderOAuthSession(ownerEmail, orgId))) {
|
|
31
|
+
const session = await getBuilderOAuthSession(ownerEmail, orgId);
|
|
32
|
+
if (!session) {
|
|
33
|
+
throw new Error("Builder.io access expired. Re-authorize Builder.io in Settings to continue.");
|
|
34
|
+
}
|
|
35
|
+
if (requiredScope && !session.scopes.includes(requiredScope)) {
|
|
36
|
+
throw new Error(`Builder.io access needs re-authorizing to grant ${requiredScope}. Open Settings and authorize Builder.io again.`);
|
|
37
|
+
}
|
|
38
|
+
return `Bearer ${session.accessToken}`;
|
|
39
|
+
}
|
|
40
|
+
const privateKey = await resolveBuilderPrivateKey();
|
|
41
|
+
if (!privateKey)
|
|
42
|
+
throw new Error("Builder.io is not connected.");
|
|
43
|
+
return `Bearer ${privateKey}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether Builder.io can authenticate an asset call for this request — an
|
|
47
|
+
* OAuth grant, or a legacy private key.
|
|
48
|
+
*
|
|
49
|
+
* Storage gates and provider selection use this. It intentionally does not
|
|
50
|
+
* verify the grant is usable: answering `false` for a connected user whose
|
|
51
|
+
* grant needs re-authorizing would report storage as unconfigured and send
|
|
52
|
+
* them to set up something they already have, instead of letting the upload
|
|
53
|
+
* path say what is actually wrong.
|
|
54
|
+
*/
|
|
55
|
+
export async function hasBuilderApiCredentialCustody() {
|
|
56
|
+
const ownerEmail = getRequestUserEmail();
|
|
57
|
+
if (ownerEmail &&
|
|
58
|
+
(await hasBuilderOAuthSession(ownerEmail, getRequestOrgId()))) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return !!(await resolveBuilderPrivateKey());
|
|
62
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export declare const BUILDER_OAUTH_ISSUER = "https://mcp.builder.io";
|
|
2
2
|
export declare const BUILDER_OAUTH_RESOURCE = "https://api.builder.io";
|
|
3
3
|
export declare const BUILDER_OAUTH_SCOPE = "builder:ai:invoke";
|
|
4
|
-
|
|
4
|
+
/** Enforced by Builder's `/api/v1/upload/*` endpoints; without it, no uploads. */
|
|
5
|
+
export declare const BUILDER_ASSETS_WRITE_SCOPE = "builder:assets:write";
|
|
6
|
+
export declare const BUILDER_OAUTH_SCOPES: readonly ["builder:ai:invoke", "builder:assets:write"];
|
|
5
7
|
export type BuilderOAuthPendingFlow = {
|
|
6
8
|
codeVerifier: string;
|
|
7
9
|
clientInformation: unknown;
|
|
@@ -32,8 +34,8 @@ export declare function finishBuilderOAuthAuthorization(input: {
|
|
|
32
34
|
pending: BuilderOAuthPendingFlow;
|
|
33
35
|
}): Promise<void>;
|
|
34
36
|
export declare function markBuilderOAuthReconnectRequired(ownerEmail: string): Promise<void>;
|
|
35
|
-
export declare function getBuilderOAuthSession(ownerEmail: string): Promise<BuilderOAuthSession | null>;
|
|
36
|
-
export declare function hasBuilderOAuthSession(ownerEmail: string): Promise<boolean>;
|
|
37
|
+
export declare function getBuilderOAuthSession(ownerEmail: string, orgId?: string | null): Promise<BuilderOAuthSession | null>;
|
|
38
|
+
export declare function hasBuilderOAuthSession(ownerEmail: string, orgId?: string | null): Promise<boolean>;
|
|
37
39
|
export declare function resolveBuilderOAuthRequestAccess(input: {
|
|
38
40
|
ownerEmail: string;
|
|
39
41
|
requiredScope: string;
|
|
@@ -5,7 +5,12 @@ import { resolveOrgIdForEmail } from "../org/context.js";
|
|
|
5
5
|
export const BUILDER_OAUTH_ISSUER = "https://mcp.builder.io";
|
|
6
6
|
export const BUILDER_OAUTH_RESOURCE = "https://api.builder.io";
|
|
7
7
|
export const BUILDER_OAUTH_SCOPE = "builder:ai:invoke";
|
|
8
|
-
|
|
8
|
+
/** Enforced by Builder's `/api/v1/upload/*` endpoints; without it, no uploads. */
|
|
9
|
+
export const BUILDER_ASSETS_WRITE_SCOPE = "builder:assets:write";
|
|
10
|
+
export const BUILDER_OAUTH_SCOPES = [
|
|
11
|
+
BUILDER_OAUTH_SCOPE,
|
|
12
|
+
BUILDER_ASSETS_WRITE_SCOPE,
|
|
13
|
+
];
|
|
9
14
|
// Folded with the owner so each owner gets their own (provider, account_id)
|
|
10
15
|
// row; a bare shared key would let only the first connector hold a grant.
|
|
11
16
|
const BUILDER_OAUTH_KEY = "builder-general-resource-v1";
|
|
@@ -26,12 +31,17 @@ function normalizeOwnerEmail(ownerEmail) {
|
|
|
26
31
|
}
|
|
27
32
|
// Read paths use this: a caller with no org simply has no Builder session, so
|
|
28
33
|
// engine detection and status checks get null instead of an exception.
|
|
29
|
-
|
|
34
|
+
//
|
|
35
|
+
// An explicit orgId wins over the user's active org. The grant is org-scoped,
|
|
36
|
+
// so work that carries its own organization — a recording finalizing after the
|
|
37
|
+
// user switched active org, a background run — must authorize against that org
|
|
38
|
+
// rather than whichever one the user happens to be looking at now.
|
|
39
|
+
async function resolveOwnerOptions(ownerEmail, orgId) {
|
|
30
40
|
const email = normalizeOwnerEmail(ownerEmail);
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
41
|
+
const resolvedOrgId = orgId?.trim() || (await resolveOrgIdForEmail(email));
|
|
42
|
+
if (!resolvedOrgId)
|
|
33
43
|
return null;
|
|
34
|
-
return orgOwnerOptions(
|
|
44
|
+
return orgOwnerOptions(resolvedOrgId);
|
|
35
45
|
}
|
|
36
46
|
// Write paths use this: storing a Builder credential without an org is a broken
|
|
37
47
|
// invariant, so a missing org fails loudly rather than silently dropping it.
|
|
@@ -54,10 +64,28 @@ function builderOAuthKey(orgId) {
|
|
|
54
64
|
const digest = createHash("sha256").update(orgId).digest("hex");
|
|
55
65
|
return `${BUILDER_OAUTH_KEY}:o:${digest}`;
|
|
56
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* RFC 6749 §5.1 lets a token response omit `scope` when the grant matches what
|
|
69
|
+
* was requested. Record what this flow asked for, so a stored credential always
|
|
70
|
+
* states its own scopes: inferring them later cannot tell a new two-scope grant
|
|
71
|
+
* from a pre-change AI-only one, and either guess is wrong for the other.
|
|
72
|
+
*/
|
|
73
|
+
function withRecordedScopes(credentials) {
|
|
74
|
+
if (typeof credentials.tokens.scope === "string")
|
|
75
|
+
return credentials;
|
|
76
|
+
return {
|
|
77
|
+
...credentials,
|
|
78
|
+
tokens: { ...credentials.tokens, scope: BUILDER_OAUTH_SCOPES.join(" ") },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Builder's token endpoint always sets `scope`, and `withRecordedScopes` backs
|
|
82
|
+
// that up for anything this flow stores, so an absent claim can only be a grant
|
|
83
|
+
// predating both. Those were AI-only and must not be credited with an upload
|
|
84
|
+
// scope the user never consented to.
|
|
57
85
|
function scopesFrom(credentials) {
|
|
58
86
|
const declared = credentials.tokens.scope;
|
|
59
87
|
if (typeof declared !== "string")
|
|
60
|
-
return [
|
|
88
|
+
return [BUILDER_OAUTH_SCOPE];
|
|
61
89
|
return declared.split(/\s+/).filter(Boolean);
|
|
62
90
|
}
|
|
63
91
|
function resourceUrlsMatch(left, right) {
|
|
@@ -92,7 +120,7 @@ export async function startBuilderOAuthAuthorization(input) {
|
|
|
92
120
|
serverUrl: BUILDER_OAUTH_RESOURCE,
|
|
93
121
|
redirectUrl: input.redirectUri,
|
|
94
122
|
state: input.state,
|
|
95
|
-
scope:
|
|
123
|
+
scope: BUILDER_OAUTH_SCOPES.join(" "),
|
|
96
124
|
resourceMetadataUrl: BUILDER_OAUTH_PROTECTED_RESOURCE_METADATA,
|
|
97
125
|
});
|
|
98
126
|
return {
|
|
@@ -124,7 +152,7 @@ export async function finishBuilderOAuthAuthorization(input) {
|
|
|
124
152
|
...(input.orgId
|
|
125
153
|
? orgOwnerOptions(input.orgId)
|
|
126
154
|
: await ownerOptions(input.ownerEmail)),
|
|
127
|
-
credentials: result.credentials,
|
|
155
|
+
credentials: withRecordedScopes(result.credentials),
|
|
128
156
|
});
|
|
129
157
|
}
|
|
130
158
|
export async function markBuilderOAuthReconnectRequired(ownerEmail) {
|
|
@@ -133,8 +161,8 @@ export async function markBuilderOAuthReconnectRequired(ownerEmail) {
|
|
|
133
161
|
return;
|
|
134
162
|
await markMcpOAuthReconnectRequired(options);
|
|
135
163
|
}
|
|
136
|
-
export async function getBuilderOAuthSession(ownerEmail) {
|
|
137
|
-
const options = await resolveOwnerOptions(ownerEmail);
|
|
164
|
+
export async function getBuilderOAuthSession(ownerEmail, orgId) {
|
|
165
|
+
const options = await resolveOwnerOptions(ownerEmail, orgId);
|
|
138
166
|
if (!options)
|
|
139
167
|
return null;
|
|
140
168
|
// Delegates refresh single-flight and reconnect latching to the shared
|
|
@@ -152,8 +180,8 @@ export async function getBuilderOAuthSession(ownerEmail) {
|
|
|
152
180
|
scopes: scopesFrom(credentials),
|
|
153
181
|
};
|
|
154
182
|
}
|
|
155
|
-
export async function hasBuilderOAuthSession(ownerEmail) {
|
|
156
|
-
const options = await resolveOwnerOptions(ownerEmail);
|
|
183
|
+
export async function hasBuilderOAuthSession(ownerEmail, orgId) {
|
|
184
|
+
const options = await resolveOwnerOptions(ownerEmail, orgId);
|
|
157
185
|
if (!options)
|
|
158
186
|
return false;
|
|
159
187
|
const stored = await getOAuthTokens("mcp", options.key, `${options.scope}:${options.scopeId}`);
|
|
@@ -2747,8 +2747,11 @@ export function createCoreRoutesPlugin(options = {}) {
|
|
|
2747
2747
|
const active = await getActiveFileUploadProviderForRequest();
|
|
2748
2748
|
let builderConfigured = !!process.env.BUILDER_PRIVATE_KEY;
|
|
2749
2749
|
try {
|
|
2750
|
-
|
|
2751
|
-
|
|
2750
|
+
// Must match what provider selection asks, or this reports
|
|
2751
|
+
// storage as unconfigured for an OAuth-only connection whose
|
|
2752
|
+
// uploads actually work.
|
|
2753
|
+
const { hasBuilderApiCredentialCustody } = await import("./builder-api-auth.js");
|
|
2754
|
+
builderConfigured = await hasBuilderApiCredentialCustody();
|
|
2752
2755
|
}
|
|
2753
2756
|
catch {
|
|
2754
2757
|
// fall back to env check above
|