@agent-native/core 0.84.20 → 0.84.21
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/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/caller-auth.ts +37 -8
- package/corpus/core/src/a2a/client.ts +178 -67
- package/corpus/core/src/agent/production-agent.ts +19 -3
- package/corpus/core/src/agent/types.ts +1 -1
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
- package/corpus/core/src/cli/recap.ts +43 -34
- package/corpus/core/src/client/PoweredByBadge.tsx +70 -17
- package/corpus/core/src/client/agent-chat-adapter.ts +5 -1
- package/corpus/core/src/client/i18n.tsx +10 -5
- package/corpus/core/src/client/sse-event-processor.ts +147 -5
- package/corpus/core/src/file-upload/builder.ts +35 -14
- package/corpus/core/src/file-upload/types.ts +3 -0
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +49 -24
- package/corpus/core/src/mcp/builtin-tools.ts +3 -0
- package/corpus/core/src/mcp/org-directory.ts +66 -20
- package/corpus/core/src/scripts/call-agent.ts +7 -2
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +161 -12
- package/corpus/templates/analytics/changelog/2026-07-01-session-replays-load-large-production-recordings-from-scoped.md +6 -0
- package/corpus/templates/calendar/app/pages/BookingPage.tsx +14 -8
- package/corpus/templates/calendar/changelog/2026-07-01-meeting-invite-pages-have-cleaner-branding-a-black-dark-mo.md +6 -0
- package/corpus/templates/clips/actions/create-recording.ts +7 -2
- package/corpus/templates/clips/actions/finalize-recording.ts +11 -0
- package/corpus/templates/clips/actions/get-recording-player-data.ts +4 -2
- package/corpus/templates/clips/actions/import-loom-recording.ts +1 -0
- package/corpus/templates/clips/actions/lib/loom-video.ts +16 -0
- package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -2
- package/corpus/templates/clips/app/components/recorder/countdown-overlay.tsx +4 -3
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +39 -12
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +65 -1
- package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +1 -22
- package/corpus/templates/clips/app/routes/record.tsx +48 -8
- package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-loom-imports-finish-sooner-by-skipping-the-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-clip-uploads-and-playback-recover-more-reliably-from-storage.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-desktop-recording-avoids-unsupported-browser-tab-capture.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-embedded-player-progress-is-easier-to-see-on-dark-videos.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-loom-imports-show-clear-guidance-when-loom-does-not-provide-a-downloadable-video.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-preserve-in-flight-streaming-uploads-when-the-streaming-kill.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-recording-controls-are-cleaner-and-the-countdown-uses-shadow.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-01-recordings-now-recover-when-final-upload-status-gets-lost.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +7 -224
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +64 -6
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +87 -5
- package/corpus/templates/clips/server/lib/streaming-upload-mode.ts +7 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +4 -1
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +96 -1
- package/corpus/templates/clips/shared/finalize-recovery.ts +242 -0
- package/corpus/templates/design/changelog/2026-07-01-design-recovers-sooner-when-an-agent-gets-stuck-preparing-a-.md +6 -0
- package/corpus/templates/plan/app/global.css +1 -1
- package/dist/a2a/caller-auth.d.ts +1 -0
- package/dist/a2a/caller-auth.d.ts.map +1 -1
- package/dist/a2a/caller-auth.js +34 -8
- package/dist/a2a/caller-auth.js.map +1 -1
- package/dist/a2a/client.d.ts +4 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +148 -58
- package/dist/a2a/client.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +19 -4
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/types.d.ts +1 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +3 -3
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +41 -34
- package/dist/cli/recap.js.map +1 -1
- package/dist/client/PoweredByBadge.d.ts +9 -3
- package/dist/client/PoweredByBadge.d.ts.map +1 -1
- package/dist/client/PoweredByBadge.js +47 -15
- package/dist/client/PoweredByBadge.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +5 -1
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/i18n.d.ts +1 -1
- package/dist/client/i18n.d.ts.map +1 -1
- package/dist/client/i18n.js +6 -2
- package/dist/client/i18n.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts +2 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +121 -5
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/builder.d.ts.map +1 -1
- package/dist/file-upload/builder.js +22 -8
- package/dist/file-upload/builder.js.map +1 -1
- package/dist/file-upload/types.d.ts +5 -1
- package/dist/file-upload/types.d.ts.map +1 -1
- package/dist/file-upload/types.js.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +43 -18
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +3 -0
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/mcp/org-directory.d.ts.map +1 -1
- package/dist/mcp/org-directory.js +58 -20
- package/dist/mcp/org-directory.js.map +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +2 -2
- package/dist/scripts/call-agent.js.map +1 -1
- package/package.json +1 -1
|
@@ -51,6 +51,7 @@ import { invoke } from "@tauri-apps/api/core";
|
|
|
51
51
|
import { emit, listen, type UnlistenFn } from "@tauri-apps/api/event";
|
|
52
52
|
import { open as openExternal } from "@tauri-apps/plugin-shell";
|
|
53
53
|
|
|
54
|
+
import { waitForReadyRecordingAfterFinalizeError } from "../../../shared/finalize-recovery";
|
|
54
55
|
import type { LocalRecordingMode } from "../shared/config";
|
|
55
56
|
import { createAudioCue, type AudioCue } from "./audio-cue";
|
|
56
57
|
import { createCameraCompositeStream } from "./camera-composite";
|
|
@@ -718,6 +719,28 @@ async function markBrowserRecordingBackupError(
|
|
|
718
719
|
});
|
|
719
720
|
}
|
|
720
721
|
|
|
722
|
+
async function recoverReadyRecordingAfterFinalizeError({
|
|
723
|
+
serverUrl,
|
|
724
|
+
recordingId,
|
|
725
|
+
authToken,
|
|
726
|
+
}: {
|
|
727
|
+
serverUrl: string;
|
|
728
|
+
recordingId: string;
|
|
729
|
+
authToken?: string;
|
|
730
|
+
}): Promise<boolean> {
|
|
731
|
+
const recovered = await waitForReadyRecordingAfterFinalizeError({
|
|
732
|
+
uploadUrl: chunkUrl(serverUrl, recordingId, 0, false),
|
|
733
|
+
recordingId,
|
|
734
|
+
authToken,
|
|
735
|
+
preferAuthenticated: true,
|
|
736
|
+
});
|
|
737
|
+
if (!recovered) return false;
|
|
738
|
+
await deleteBrowserRecordingBackup(recordingId).catch((err) => {
|
|
739
|
+
console.warn("[clips-recorder] recovered backup cleanup failed:", err);
|
|
740
|
+
});
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
743
|
+
|
|
721
744
|
export async function listBrowserRecordingBackups(): Promise<
|
|
722
745
|
PendingBrowserRecordingUpload[]
|
|
723
746
|
> {
|
|
@@ -827,8 +850,12 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
827
850
|
);
|
|
828
851
|
}
|
|
829
852
|
|
|
830
|
-
|
|
831
|
-
|
|
853
|
+
const finalChunkUrl = chunkUrl(
|
|
854
|
+
meta.serverUrl,
|
|
855
|
+
meta.recordingId,
|
|
856
|
+
validatedChunks.length,
|
|
857
|
+
true,
|
|
858
|
+
{
|
|
832
859
|
total: String(totalPosts),
|
|
833
860
|
mimeType: meta.mimeType,
|
|
834
861
|
durationMs: String(Math.round(meta.durationMs || 0)),
|
|
@@ -836,10 +863,29 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
836
863
|
...(meta.height ? { height: String(meta.height) } : {}),
|
|
837
864
|
hasAudio: meta.hasAudio ? "1" : "0",
|
|
838
865
|
hasCamera: meta.hasCamera ? "1" : "0",
|
|
839
|
-
}
|
|
840
|
-
new Blob([], { type: meta.mimeType }),
|
|
841
|
-
input.authToken,
|
|
866
|
+
},
|
|
842
867
|
);
|
|
868
|
+
try {
|
|
869
|
+
await postBackupChunk(
|
|
870
|
+
finalChunkUrl,
|
|
871
|
+
new Blob([], { type: meta.mimeType }),
|
|
872
|
+
input.authToken,
|
|
873
|
+
);
|
|
874
|
+
} catch (err) {
|
|
875
|
+
if (
|
|
876
|
+
await recoverReadyRecordingAfterFinalizeError({
|
|
877
|
+
serverUrl: meta.serverUrl,
|
|
878
|
+
recordingId: meta.recordingId,
|
|
879
|
+
authToken: input.authToken,
|
|
880
|
+
})
|
|
881
|
+
) {
|
|
882
|
+
return {
|
|
883
|
+
recordingId: meta.recordingId,
|
|
884
|
+
viewUrl: `/r/${meta.recordingId}`,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
throw err;
|
|
888
|
+
}
|
|
843
889
|
|
|
844
890
|
await deleteBrowserRecordingBackup(meta.recordingId);
|
|
845
891
|
return { recordingId: meta.recordingId, viewUrl: `/r/${meta.recordingId}` };
|
|
@@ -1986,6 +2032,22 @@ async function startNativeFullscreenRecording(
|
|
|
1986
2032
|
try {
|
|
1987
2033
|
uploadResult = await uploadPromise;
|
|
1988
2034
|
} catch (err) {
|
|
2035
|
+
if (
|
|
2036
|
+
await recoverReadyRecordingAfterFinalizeError({
|
|
2037
|
+
serverUrl: params.serverUrl,
|
|
2038
|
+
recordingId: id,
|
|
2039
|
+
authToken: params.authToken,
|
|
2040
|
+
})
|
|
2041
|
+
) {
|
|
2042
|
+
try {
|
|
2043
|
+
await openExternal(
|
|
2044
|
+
`${params.serverUrl.replace(/\/+$/, "")}${viewUrl}`,
|
|
2045
|
+
);
|
|
2046
|
+
} catch (openErr) {
|
|
2047
|
+
console.error("[clips-recorder] openExternal failed:", openErr);
|
|
2048
|
+
}
|
|
2049
|
+
return { recordingId: id, viewUrl };
|
|
2050
|
+
}
|
|
1989
2051
|
await abortRecordingUpload(
|
|
1990
2052
|
params.serverUrl,
|
|
1991
2053
|
id,
|
|
@@ -3225,6 +3287,26 @@ async function startRecordingInner(
|
|
|
3225
3287
|
} catch (err) {
|
|
3226
3288
|
console.error("[clips-recorder] finalize fetch failed:", err);
|
|
3227
3289
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
3290
|
+
if (
|
|
3291
|
+
await recoverReadyRecordingAfterFinalizeError({
|
|
3292
|
+
serverUrl: params.serverUrl,
|
|
3293
|
+
recordingId: id,
|
|
3294
|
+
authToken: params.authToken,
|
|
3295
|
+
})
|
|
3296
|
+
) {
|
|
3297
|
+
const viewUrl = `/r/${id}`;
|
|
3298
|
+
try {
|
|
3299
|
+
await openExternal(
|
|
3300
|
+
`${params.serverUrl.replace(/\/+$/, "")}${viewUrl}`,
|
|
3301
|
+
);
|
|
3302
|
+
} catch (openErr) {
|
|
3303
|
+
console.error("[clips-recorder] openExternal failed:", openErr);
|
|
3304
|
+
}
|
|
3305
|
+
invoke("hide_finalizing").catch((hideErr) =>
|
|
3306
|
+
console.error("[clips-recorder] hide_finalizing failed:", hideErr),
|
|
3307
|
+
);
|
|
3308
|
+
return { recordingId: id, viewUrl };
|
|
3309
|
+
}
|
|
3228
3310
|
await markBrowserRecordingBackupError(id, error.message).catch(
|
|
3229
3311
|
() => {},
|
|
3230
3312
|
);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Temporary kill switch: streaming resumable uploads caused a spike of
|
|
2
|
+
// recording issues. Set CLIPS_DISABLE_STREAMING_UPLOAD=false to re-enable
|
|
3
|
+
// once the root cause is fixed. Defaults to disabled (streaming off).
|
|
4
|
+
export function isStreamingUploadDisabled(): boolean {
|
|
5
|
+
const flag = process.env.CLIPS_DISABLE_STREAMING_UPLOAD ?? "";
|
|
6
|
+
return flag.toLowerCase() !== "false";
|
|
7
|
+
}
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
setResumableSession,
|
|
47
47
|
type StoredResumableSession,
|
|
48
48
|
} from "../../../../lib/resumable-session.js";
|
|
49
|
+
import { isStreamingUploadDisabled } from "../../../../lib/streaming-upload-mode.js";
|
|
49
50
|
import {
|
|
50
51
|
shouldRejectVideoUploadWithoutStorage,
|
|
51
52
|
STORAGE_SETUP_REQUIRED_REASON,
|
|
@@ -174,6 +175,11 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
174
175
|
|
|
175
176
|
// Resumable streaming path — forward chunks directly to the provider.
|
|
176
177
|
const resumableSession = await getResumableSession(recordingId);
|
|
178
|
+
if (resumableSession && isStreamingUploadDisabled()) {
|
|
179
|
+
console.warn(
|
|
180
|
+
`[chunk] streaming uploads are disabled, but preserving existing resumable session for in-flight recording: ${recordingId}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
177
183
|
if (resumableSession) {
|
|
178
184
|
return handleResumableChunk(
|
|
179
185
|
event,
|
|
@@ -574,6 +580,13 @@ async function handleResumableChunk(
|
|
|
574
580
|
}
|
|
575
581
|
|
|
576
582
|
if (isFinal && bytes.byteLength === 0) {
|
|
583
|
+
if (session.bytesUploaded <= 0) {
|
|
584
|
+
setResponseStatus(event, 400);
|
|
585
|
+
return {
|
|
586
|
+
ok: false,
|
|
587
|
+
error: "Cannot finalize an empty resumable upload",
|
|
588
|
+
};
|
|
589
|
+
}
|
|
577
590
|
// 0-byte sentinel from the recorder after stop(). All data chunks have
|
|
578
591
|
// already been PUT to the provider; send Content-Range: bytes */<total>
|
|
579
592
|
// to close the session before handing off to finalize-recording.
|
|
@@ -582,7 +595,7 @@ async function handleResumableChunk(
|
|
|
582
595
|
`bytes */${session.bytesUploaded}`,
|
|
583
596
|
new Uint8Array(0),
|
|
584
597
|
);
|
|
585
|
-
if (!closeRes.ok) {
|
|
598
|
+
if (!closeRes.ok || closeRes.status === 308) {
|
|
586
599
|
console.error(
|
|
587
600
|
`[resumable-chunk-${recordingId}] session close failed (${closeRes.status})`,
|
|
588
601
|
);
|
|
@@ -59,7 +59,10 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
59
59
|
recording: {
|
|
60
60
|
id: recording.id,
|
|
61
61
|
status: recording.status,
|
|
62
|
-
videoUrl: resolvePlayerVideoUrl(recording, {
|
|
62
|
+
videoUrl: resolvePlayerVideoUrl(recording, {
|
|
63
|
+
appPath,
|
|
64
|
+
proxyRemoteMedia: true,
|
|
65
|
+
}),
|
|
63
66
|
durationMs: recording.durationMs,
|
|
64
67
|
width: recording.width,
|
|
65
68
|
height: recording.height,
|
|
@@ -86,6 +86,8 @@ const PROXIED_HEADER_NAMES = [
|
|
|
86
86
|
const PROVIDER_MEDIA_FETCH_TIMEOUT_MS = 30_000;
|
|
87
87
|
const PROTECTED_MEDIA_ACCESS_TTL_SECONDS = 6 * 60 * 60;
|
|
88
88
|
const PROTECTED_MEDIA_COOKIE_PREFIX = "clips_media_";
|
|
89
|
+
const COMPRESSED_BUILDER_MEDIA_MISS_TTL_MS = 5_000;
|
|
90
|
+
const compressedBuilderMediaMisses = new Map<string, number>();
|
|
89
91
|
|
|
90
92
|
function appPath(path: string): string {
|
|
91
93
|
if (!path.startsWith("/")) return path;
|
|
@@ -145,6 +147,82 @@ function isRecursiveVideoRouteUrl(value: string, recordingId: string): boolean {
|
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
function compressedBuilderMediaUrl(sourceUrl: string): string | null {
|
|
151
|
+
try {
|
|
152
|
+
const url = new URL(sourceUrl);
|
|
153
|
+
if (!/^cdn(?:-qa)?\.builder\.io$/i.test(url.hostname)) return null;
|
|
154
|
+
if (url.searchParams.get("optimized") === "true") return null;
|
|
155
|
+
|
|
156
|
+
let objectPath: string | null = null;
|
|
157
|
+
if (url.pathname.startsWith("/o/")) {
|
|
158
|
+
objectPath = decodeURIComponent(url.pathname.slice("/o/".length));
|
|
159
|
+
} else if (url.pathname.startsWith("/api/v1/file/")) {
|
|
160
|
+
objectPath = decodeURIComponent(
|
|
161
|
+
url.pathname.slice("/api/v1/file/".length),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (!objectPath || !objectPath.startsWith("assets/")) return null;
|
|
165
|
+
if (objectPath.endsWith("/compressed")) return null;
|
|
166
|
+
|
|
167
|
+
const parts = objectPath.split("/");
|
|
168
|
+
const assetId = parts[parts.length - 1];
|
|
169
|
+
const apiKey = url.searchParams.get("apiKey") || parts[1];
|
|
170
|
+
if (!assetId || !apiKey) return null;
|
|
171
|
+
|
|
172
|
+
const compressedPath = `${objectPath}/compressed`;
|
|
173
|
+
const compressedUrl = new URL(
|
|
174
|
+
`/o/${encodeURIComponent(compressedPath)}`,
|
|
175
|
+
url.origin,
|
|
176
|
+
);
|
|
177
|
+
compressedUrl.searchParams.set("apiKey", apiKey);
|
|
178
|
+
compressedUrl.searchParams.set(
|
|
179
|
+
"token",
|
|
180
|
+
url.searchParams.get("token") || assetId,
|
|
181
|
+
);
|
|
182
|
+
compressedUrl.searchParams.set("alt", "media");
|
|
183
|
+
compressedUrl.searchParams.set("optimized", "true");
|
|
184
|
+
return compressedUrl.toString();
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function shouldSkipCompressedBuilderMediaProbe(
|
|
191
|
+
compressedSourceUrl: string,
|
|
192
|
+
): boolean {
|
|
193
|
+
const expiresAt = compressedBuilderMediaMisses.get(compressedSourceUrl);
|
|
194
|
+
if (!expiresAt) return false;
|
|
195
|
+
if (expiresAt <= Date.now()) {
|
|
196
|
+
compressedBuilderMediaMisses.delete(compressedSourceUrl);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function rememberCompressedBuilderMediaMiss(compressedSourceUrl: string): void {
|
|
203
|
+
if (compressedBuilderMediaMisses.size > 1_000) {
|
|
204
|
+
for (const [url, expiresAt] of compressedBuilderMediaMisses) {
|
|
205
|
+
if (expiresAt <= Date.now()) compressedBuilderMediaMisses.delete(url);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
compressedBuilderMediaMisses.set(
|
|
209
|
+
compressedSourceUrl,
|
|
210
|
+
Date.now() + COMPRESSED_BUILDER_MEDIA_MISS_TTL_MS,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function shouldFallbackToOriginalMedia(
|
|
215
|
+
upstream: Response | { error: string; status: number },
|
|
216
|
+
): boolean {
|
|
217
|
+
return [403, 404, 416].includes(upstream.status);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function shouldRememberCompressedBuilderMediaMiss(
|
|
221
|
+
upstream: Response | { error: string; status: number },
|
|
222
|
+
): boolean {
|
|
223
|
+
return [403, 404].includes(upstream.status);
|
|
224
|
+
}
|
|
225
|
+
|
|
148
226
|
async function fetchProviderMedia(
|
|
149
227
|
sourceUrl: string,
|
|
150
228
|
rangeHeader: string | undefined,
|
|
@@ -434,7 +512,24 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
434
512
|
|
|
435
513
|
let upstream: Response | { error: string; status: number };
|
|
436
514
|
try {
|
|
437
|
-
|
|
515
|
+
const compressedSourceUrl = compressedBuilderMediaUrl(sourceUrl);
|
|
516
|
+
if (
|
|
517
|
+
compressedSourceUrl &&
|
|
518
|
+
!shouldSkipCompressedBuilderMediaProbe(compressedSourceUrl)
|
|
519
|
+
) {
|
|
520
|
+
upstream = await fetchProviderMedia(
|
|
521
|
+
compressedSourceUrl,
|
|
522
|
+
rangeHeader,
|
|
523
|
+
);
|
|
524
|
+
if (shouldRememberCompressedBuilderMediaMiss(upstream)) {
|
|
525
|
+
rememberCompressedBuilderMediaMiss(compressedSourceUrl);
|
|
526
|
+
}
|
|
527
|
+
if (shouldFallbackToOriginalMedia(upstream)) {
|
|
528
|
+
upstream = await fetchProviderMedia(sourceUrl, rangeHeader);
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
upstream = await fetchProviderMedia(sourceUrl, rangeHeader);
|
|
532
|
+
}
|
|
438
533
|
} catch (err) {
|
|
439
534
|
setResponseStatus(event, statusCodeForProviderFetchError(err));
|
|
440
535
|
return { error: messageForProviderFetchError(err) };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
type FetchLike = (
|
|
2
|
+
input: RequestInfo | URL,
|
|
3
|
+
init?: RequestInit,
|
|
4
|
+
) => Promise<Response>;
|
|
5
|
+
|
|
6
|
+
export type RecoveredReadyRecording = {
|
|
7
|
+
ok: true;
|
|
8
|
+
finalized: true;
|
|
9
|
+
recoveredAfterFinalizeError: true;
|
|
10
|
+
id: string;
|
|
11
|
+
recordingId: string;
|
|
12
|
+
status: "ready";
|
|
13
|
+
videoUrl: string;
|
|
14
|
+
durationMs?: number;
|
|
15
|
+
width?: number;
|
|
16
|
+
height?: number;
|
|
17
|
+
hasAudio?: boolean;
|
|
18
|
+
hasCamera?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ProbeResult =
|
|
22
|
+
| { ready: true; result: RecoveredReadyRecording }
|
|
23
|
+
| { ready: false; terminal: boolean; status?: string };
|
|
24
|
+
|
|
25
|
+
const DEFAULT_READY_RECOVERY_TIMEOUT_MS = 90_000;
|
|
26
|
+
const DEFAULT_READY_RECOVERY_INTERVAL_MS = 3_000;
|
|
27
|
+
const DEFAULT_READY_RECOVERY_FETCH_TIMEOUT_MS = 2_000;
|
|
28
|
+
|
|
29
|
+
function sleep(ms: number): Promise<void> {
|
|
30
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function absoluteUploadUrl(uploadUrl: string): URL {
|
|
34
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(uploadUrl)) return new URL(uploadUrl);
|
|
35
|
+
const origin =
|
|
36
|
+
typeof globalThis.location?.origin === "string"
|
|
37
|
+
? globalThis.location.origin
|
|
38
|
+
: "http://localhost";
|
|
39
|
+
return new URL(uploadUrl, origin);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function maybeRelativeUrl(url: URL, original: string): string {
|
|
43
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(original)) return url.toString();
|
|
44
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function publicRecordingStatusUrl(
|
|
48
|
+
uploadUrl: string,
|
|
49
|
+
recordingId: string,
|
|
50
|
+
): string {
|
|
51
|
+
const url = absoluteUploadUrl(uploadUrl);
|
|
52
|
+
const match = url.pathname.match(/^(.*)\/api\/uploads\/[^/]+\/chunk$/);
|
|
53
|
+
const basePath = match?.[1] ?? "";
|
|
54
|
+
url.pathname = `${basePath}/api/public-recording`;
|
|
55
|
+
url.search = "";
|
|
56
|
+
url.searchParams.set("id", recordingId);
|
|
57
|
+
return maybeRelativeUrl(url, uploadUrl);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function authenticatedRecordingStatusUrl(
|
|
61
|
+
uploadUrl: string,
|
|
62
|
+
recordingId: string,
|
|
63
|
+
): string {
|
|
64
|
+
const url = absoluteUploadUrl(uploadUrl);
|
|
65
|
+
const match = url.pathname.match(/^(.*)\/api\/uploads\/[^/]+\/chunk$/);
|
|
66
|
+
const basePath = match?.[1] ?? "";
|
|
67
|
+
url.pathname = `${basePath}/api/uploads/${encodeURIComponent(
|
|
68
|
+
recordingId,
|
|
69
|
+
)}/status`;
|
|
70
|
+
url.search = "";
|
|
71
|
+
return maybeRelativeUrl(url, uploadUrl);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function optionalNumber(value: unknown): number | undefined {
|
|
75
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
76
|
+
? value
|
|
77
|
+
: undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function optionalBoolean(value: unknown): boolean | undefined {
|
|
81
|
+
return typeof value === "boolean" ? value : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readyRecordingFromPublicPayload(
|
|
85
|
+
payload: unknown,
|
|
86
|
+
fallbackRecordingId: string,
|
|
87
|
+
): ProbeResult {
|
|
88
|
+
const root =
|
|
89
|
+
payload && typeof payload === "object"
|
|
90
|
+
? (payload as Record<string, unknown>)
|
|
91
|
+
: null;
|
|
92
|
+
const recording =
|
|
93
|
+
root?.recording && typeof root.recording === "object"
|
|
94
|
+
? (root.recording as Record<string, unknown>)
|
|
95
|
+
: null;
|
|
96
|
+
const status =
|
|
97
|
+
typeof recording?.status === "string" ? recording.status : undefined;
|
|
98
|
+
|
|
99
|
+
if (!recording) return { ready: false, terminal: false };
|
|
100
|
+
if (status === "failed") return { ready: false, terminal: true, status };
|
|
101
|
+
|
|
102
|
+
const videoUrl =
|
|
103
|
+
typeof recording.videoUrl === "string" ? recording.videoUrl : "";
|
|
104
|
+
if (status !== "ready" || !videoUrl) {
|
|
105
|
+
return { ready: false, terminal: false, status };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const id =
|
|
109
|
+
typeof recording.id === "string" && recording.id
|
|
110
|
+
? recording.id
|
|
111
|
+
: fallbackRecordingId;
|
|
112
|
+
return {
|
|
113
|
+
ready: true,
|
|
114
|
+
result: {
|
|
115
|
+
ok: true,
|
|
116
|
+
finalized: true,
|
|
117
|
+
recoveredAfterFinalizeError: true,
|
|
118
|
+
id,
|
|
119
|
+
recordingId: id,
|
|
120
|
+
status: "ready",
|
|
121
|
+
videoUrl,
|
|
122
|
+
durationMs: optionalNumber(recording.durationMs),
|
|
123
|
+
width: optionalNumber(recording.width),
|
|
124
|
+
height: optionalNumber(recording.height),
|
|
125
|
+
hasAudio: optionalBoolean(recording.hasAudio),
|
|
126
|
+
hasCamera: optionalBoolean(recording.hasCamera),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function fetchWithTimeout(
|
|
132
|
+
fetchImpl: FetchLike,
|
|
133
|
+
url: string,
|
|
134
|
+
init: RequestInit,
|
|
135
|
+
timeoutMs: number,
|
|
136
|
+
): Promise<Response> {
|
|
137
|
+
if (timeoutMs <= 0 || typeof AbortController === "undefined") {
|
|
138
|
+
return fetchImpl(url, init);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
143
|
+
try {
|
|
144
|
+
return await fetchImpl(url, { ...init, signal: controller.signal });
|
|
145
|
+
} finally {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function waitForReadyRecordingAfterFinalizeError(args: {
|
|
151
|
+
uploadUrl: string;
|
|
152
|
+
recordingId: string;
|
|
153
|
+
authToken?: string | null;
|
|
154
|
+
preferAuthenticated?: boolean;
|
|
155
|
+
fetchImpl?: FetchLike;
|
|
156
|
+
sleepImpl?: (ms: number) => Promise<void>;
|
|
157
|
+
timeoutMs?: number;
|
|
158
|
+
intervalMs?: number;
|
|
159
|
+
fetchTimeoutMs?: number;
|
|
160
|
+
}): Promise<RecoveredReadyRecording | null> {
|
|
161
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
162
|
+
const sleepImpl = args.sleepImpl ?? sleep;
|
|
163
|
+
const timeoutMs = Math.max(
|
|
164
|
+
1,
|
|
165
|
+
args.timeoutMs ?? DEFAULT_READY_RECOVERY_TIMEOUT_MS,
|
|
166
|
+
);
|
|
167
|
+
const intervalMs = Math.max(
|
|
168
|
+
1,
|
|
169
|
+
args.intervalMs ?? DEFAULT_READY_RECOVERY_INTERVAL_MS,
|
|
170
|
+
);
|
|
171
|
+
const attempts = Math.max(1, Math.ceil(timeoutMs / intervalMs));
|
|
172
|
+
let url: string;
|
|
173
|
+
let authenticatedUrl: string | null = null;
|
|
174
|
+
try {
|
|
175
|
+
url = publicRecordingStatusUrl(args.uploadUrl, args.recordingId);
|
|
176
|
+
authenticatedUrl =
|
|
177
|
+
args.authToken || args.preferAuthenticated
|
|
178
|
+
? authenticatedRecordingStatusUrl(args.uploadUrl, args.recordingId)
|
|
179
|
+
: null;
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
185
|
+
const urls = authenticatedUrl
|
|
186
|
+
? [
|
|
187
|
+
{ url: authenticatedUrl, authenticated: true },
|
|
188
|
+
{ url, authenticated: false },
|
|
189
|
+
]
|
|
190
|
+
: [{ url, authenticated: false }];
|
|
191
|
+
|
|
192
|
+
for (const endpoint of urls) {
|
|
193
|
+
const headers: Record<string, string> = {
|
|
194
|
+
Accept: "application/json",
|
|
195
|
+
"X-Agent-Native-Frontend": "1",
|
|
196
|
+
};
|
|
197
|
+
if (endpoint.authenticated && args.authToken) {
|
|
198
|
+
headers.Authorization = `Bearer ${args.authToken}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const response = await fetchWithTimeout(
|
|
203
|
+
fetchImpl,
|
|
204
|
+
endpoint.url,
|
|
205
|
+
{
|
|
206
|
+
method: "GET",
|
|
207
|
+
headers,
|
|
208
|
+
credentials: "include",
|
|
209
|
+
cache: "no-store",
|
|
210
|
+
},
|
|
211
|
+
args.fetchTimeoutMs ?? DEFAULT_READY_RECOVERY_FETCH_TIMEOUT_MS,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (response.ok) {
|
|
215
|
+
const payload = await response.json().catch(() => null);
|
|
216
|
+
const probe = readyRecordingFromPublicPayload(
|
|
217
|
+
payload,
|
|
218
|
+
args.recordingId,
|
|
219
|
+
);
|
|
220
|
+
if (probe.ready) return probe.result;
|
|
221
|
+
if (probe.terminal) return null;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
response.status >= 400 &&
|
|
227
|
+
response.status < 500 &&
|
|
228
|
+
!endpoint.authenticated &&
|
|
229
|
+
!authenticatedUrl
|
|
230
|
+
) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
// Try the public endpoint fallback below, then keep polling.
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (attempt < attempts - 1) await sleepImpl(intervalMs);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"caller-auth.d.ts","sourceRoot":"","sources":["../../src/a2a/caller-auth.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,wBAAsB,oBAAoB,CAAC,OAAO,CAAC,EAAE;IACnD,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,GAAG,OAAO,CAAC,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"caller-auth.d.ts","sourceRoot":"","sources":["../../src/a2a/caller-auth.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,wBAAsB,oBAAoB,CAAC,OAAO,CAAC,EAAE;IACnD,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,GAAG,OAAO,CAAC,aAAa,CAAC,CA+DzB"}
|
package/dist/a2a/caller-auth.js
CHANGED
|
@@ -23,20 +23,46 @@ export async function resolveA2ACallerAuth(options) {
|
|
|
23
23
|
}
|
|
24
24
|
catch { }
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
const apiKeyAttempts = [];
|
|
27
|
+
const addApiKeyAttempt = (token) => {
|
|
28
|
+
if (!token || apiKeyAttempts.includes(token))
|
|
29
|
+
return;
|
|
30
|
+
apiKeyAttempts.push(token);
|
|
31
|
+
};
|
|
27
32
|
if (userEmail && (orgSecret || process.env.A2A_SECRET)) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
if (process.env.A2A_SECRET?.trim()) {
|
|
34
|
+
try {
|
|
35
|
+
addApiKeyAttempt(await signA2AToken(userEmail, orgDomain, orgSecret, {
|
|
36
|
+
expiresIn: options?.expiresIn ?? DEFAULT_A2A_CALLER_TOKEN_TTL,
|
|
37
|
+
preferGlobalSecret: true,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
}
|
|
42
|
+
if (orgSecret) {
|
|
43
|
+
try {
|
|
44
|
+
addApiKeyAttempt(await signA2AToken(userEmail, orgDomain, orgSecret, {
|
|
45
|
+
expiresIn: options?.expiresIn ?? DEFAULT_A2A_CALLER_TOKEN_TTL,
|
|
46
|
+
preferGlobalSecret: false,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
33
50
|
}
|
|
34
|
-
catch { }
|
|
35
51
|
}
|
|
36
52
|
if (options?.includeGoogleToken) {
|
|
37
53
|
await attachGoogleTokenMetadata(metadata, userEmail);
|
|
38
54
|
}
|
|
39
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
apiKey: apiKeyAttempts[0],
|
|
57
|
+
...(apiKeyAttempts.length > 1
|
|
58
|
+
? { apiKeyFallbacks: apiKeyAttempts.slice(1) }
|
|
59
|
+
: {}),
|
|
60
|
+
userEmail,
|
|
61
|
+
orgId,
|
|
62
|
+
orgDomain,
|
|
63
|
+
orgSecret,
|
|
64
|
+
metadata,
|
|
65
|
+
};
|
|
40
66
|
}
|
|
41
67
|
async function attachGoogleTokenMetadata(metadata, userEmail) {
|
|
42
68
|
if (process.env.NODE_ENV !== "production" || !userEmail)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"caller-auth.js","sourceRoot":"","sources":["../../src/a2a/caller-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,mBAAmB,GACpB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,4BAA4B,GAAG,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"caller-auth.js","sourceRoot":"","sources":["../../src/a2a/caller-auth.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n getRequestOrgId,\n getRequestUserEmail,\n} from \"../server/request-context.js\";\nimport { signA2AToken } from \"./client.js\";\n\nconst DEFAULT_A2A_CALLER_TOKEN_TTL = \"30m\";\n\nexport interface A2ACallerAuth {\n apiKey?: string;\n apiKeyFallbacks?: string[];\n userEmail?: string;\n orgId?: string;\n orgDomain?: string;\n orgSecret?: string;\n metadata: Record<string, unknown>;\n}\n\nexport async function resolveA2ACallerAuth(options?: {\n expiresIn?: string | number;\n includeGoogleToken?: boolean;\n}): Promise<A2ACallerAuth> {\n const userEmail = getRequestUserEmail();\n const metadata: Record<string, unknown> = {};\n if (userEmail) metadata.userEmail = userEmail;\n\n let orgDomain: string | undefined;\n let orgSecret: string | undefined;\n const orgId = getRequestOrgId();\n if (orgId) {\n try {\n const { getOrgDomain } = await import(\"../org/context.js\");\n orgDomain = (await getOrgDomain(orgId)) ?? undefined;\n if (orgDomain) metadata.orgDomain = orgDomain;\n } catch {}\n try {\n const { getOrgA2ASecret } = await import(\"../org/context.js\");\n orgSecret = (await getOrgA2ASecret(orgId)) ?? undefined;\n } catch {}\n }\n\n const apiKeyAttempts: string[] = [];\n const addApiKeyAttempt = (token: string | undefined) => {\n if (!token || apiKeyAttempts.includes(token)) return;\n apiKeyAttempts.push(token);\n };\n if (userEmail && (orgSecret || process.env.A2A_SECRET)) {\n if (process.env.A2A_SECRET?.trim()) {\n try {\n addApiKeyAttempt(\n await signA2AToken(userEmail, orgDomain, orgSecret, {\n expiresIn: options?.expiresIn ?? DEFAULT_A2A_CALLER_TOKEN_TTL,\n preferGlobalSecret: true,\n }),\n );\n } catch {}\n }\n if (orgSecret) {\n try {\n addApiKeyAttempt(\n await signA2AToken(userEmail, orgDomain, orgSecret, {\n expiresIn: options?.expiresIn ?? DEFAULT_A2A_CALLER_TOKEN_TTL,\n preferGlobalSecret: false,\n }),\n );\n } catch {}\n }\n }\n\n if (options?.includeGoogleToken) {\n await attachGoogleTokenMetadata(metadata, userEmail);\n }\n\n return {\n apiKey: apiKeyAttempts[0],\n ...(apiKeyAttempts.length > 1\n ? { apiKeyFallbacks: apiKeyAttempts.slice(1) }\n : {}),\n userEmail,\n orgId,\n orgDomain,\n orgSecret,\n metadata,\n };\n}\n\nasync function attachGoogleTokenMetadata(\n metadata: Record<string, unknown>,\n userEmail: string | undefined,\n): Promise<void> {\n if (process.env.NODE_ENV !== \"production\" || !userEmail) return;\n\n try {\n const { listOAuthAccountsByOwner } =\n await import(\"../oauth-tokens/store.js\");\n const accounts = await listOAuthAccountsByOwner(\"google\", userEmail);\n const tokens = accounts[0]?.tokens;\n if (tokens?.access_token) {\n metadata.googleToken = tokens.access_token;\n }\n } catch {}\n}\n"]}
|