@agent-native/core 0.178.1-nightly-20260910175201 → 0.178.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/analytics/CHANGELOG.md +1 -0
- package/corpus/templates/calendar/CHANGELOG.md +7 -0
- package/corpus/templates/clips/CHANGELOG.md +22 -0
- package/corpus/templates/clips/actions/create-intake-agent-link.ts +114 -0
- package/corpus/templates/clips/actions/create-intake-recording.ts +208 -0
- package/corpus/templates/clips/actions/create-recording-intake-link.ts +85 -0
- package/corpus/templates/clips/app/components/bug-report/bug-report-form.tsx +7 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +22 -5
- package/corpus/templates/clips/app/hooks/use-video-storage-status.ts +2 -1
- package/corpus/templates/clips/app/routes/bug-report.done.tsx +12 -10
- package/corpus/templates/clips/app/routes/bug-report.tsx +6 -0
- package/corpus/templates/clips/app/routes/record.tsx +269 -100
- package/corpus/templates/clips/server/db/schema.ts +24 -0
- package/corpus/templates/clips/server/lib/clip-intake.ts +247 -0
- package/corpus/templates/clips/server/plugins/auth.ts +8 -0
- package/corpus/templates/clips/server/plugins/db.ts +16 -0
- package/corpus/templates/clips/server/routes/api/clip-intake.get.ts +61 -0
- package/corpus/templates/clips/server/routes/api/clip-intake.post.ts +90 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +19 -4
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +25 -10
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +21 -4
- package/corpus/templates/clips/shared/clip-intake.ts +41 -0
- package/corpus/templates/clips/shared/finalize-recovery.ts +27 -5
- package/corpus/templates/clips/shared/recording-core.ts +4 -4
- package/corpus/templates/content/CHANGELOG.md +1 -0
- package/corpus/templates/design/CHANGELOG.md +9 -4
- package/corpus/templates/factory/CHANGELOG.md +11 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/docs/content/template-clips-anonymous-intake.mdx +148 -0
- package/docs/content/template-clips-developers.mdx +4 -3
- package/docs/content/template-clips.mdx +1 -0
- package/package.json +3 -3
package/corpus/README.md
CHANGED
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
All notable user-facing changes to Agent-Native Calendar are documented here. Open it any
|
|
4
4
|
time from the command menu (Cmd+K → "What's new") or from Settings.
|
|
5
5
|
|
|
6
|
+
## 2026-09-10
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Calendar keeps shared and overlaid events read-only instead of reporting a false deletion
|
|
11
|
+
|
|
6
12
|
## 2026-09-09
|
|
7
13
|
|
|
8
14
|
### Improved
|
|
9
15
|
|
|
16
|
+
- Calendar's command menu surfaces the right actions for booking links and settings
|
|
10
17
|
- Calendar can color Google events by meeting type again
|
|
11
18
|
- Connected account avatars use a slimmer border.
|
|
12
19
|
|
|
@@ -3,12 +3,34 @@
|
|
|
3
3
|
All notable user-facing changes to Clips are documented here. Open it any time
|
|
4
4
|
from the command menu (Cmd+K → "What's new") or from Settings.
|
|
5
5
|
|
|
6
|
+
## 2026-09-10
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Builder and other MCP connections now open setup in a new tab.
|
|
11
|
+
|
|
6
12
|
## 2026-09-09
|
|
7
13
|
|
|
8
14
|
### Improved
|
|
9
15
|
|
|
10
16
|
- Attendee and viewer avatars use a slimmer border.
|
|
11
17
|
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- The Agent sidebar now fits below the app toolbar without clipping its composer.
|
|
21
|
+
- Desktop recorder switches now keep their checked state clear and legible in dark mode.
|
|
22
|
+
- Recording pages now open the same contextual Agent panel used throughout the app.
|
|
23
|
+
|
|
24
|
+
## 2026-09-08
|
|
25
|
+
|
|
26
|
+
### Improved
|
|
27
|
+
|
|
28
|
+
- The Cmd+K command menu now searches and navigates across recordings, meetings, dictations, folders, and spaces.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Fixed desktop comment threads by keeping them in a full-height viewer tab, polished the side-panel tabs and spacing, added move, minimize, and close controls to desktop Clips windows, corrected the shared New recording action’s split-button corners, made library sub-item selection clear in the sidebar, and added visible folder tiles plus scoped recording actions to library and space views.
|
|
33
|
+
|
|
12
34
|
## 2026-09-04
|
|
13
35
|
|
|
14
36
|
### Fixed
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { defineAction, fail } from "@agent-native/core/action";
|
|
2
|
+
import {
|
|
3
|
+
getRequestContext,
|
|
4
|
+
getRequestUserEmail,
|
|
5
|
+
runWithRequestContext,
|
|
6
|
+
} from "@agent-native/core/server";
|
|
7
|
+
import { and, eq } from "drizzle-orm";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
11
|
+
import { findOwnedClipIntakeSession } from "../server/lib/clip-intake.js";
|
|
12
|
+
import {
|
|
13
|
+
getActiveOrganizationId,
|
|
14
|
+
ownerEmailMatches,
|
|
15
|
+
requireOrganizationAccess,
|
|
16
|
+
} from "../server/lib/recordings.js";
|
|
17
|
+
import { BUG_REPORT_AGENT_ACCESS_TTL_SECONDS } from "../shared/bug-report.js";
|
|
18
|
+
import createRecordingAgentLink from "./create-recording-agent-link.js";
|
|
19
|
+
|
|
20
|
+
export default defineAction({
|
|
21
|
+
description:
|
|
22
|
+
"Create a temporary read-only agent link for a completed Clips intake recording. This is an authenticated host-side exchange; the anonymous intake bearer cannot mint read access.",
|
|
23
|
+
agentTool: false,
|
|
24
|
+
requiresAuth: true,
|
|
25
|
+
http: { method: "POST" },
|
|
26
|
+
maxBodyBytes: 16 * 1024,
|
|
27
|
+
schema: z.object({
|
|
28
|
+
intakeId: z.string().min(16).max(80),
|
|
29
|
+
recordingId: z.string().min(1).max(200),
|
|
30
|
+
ttlSeconds: z
|
|
31
|
+
.number()
|
|
32
|
+
.int()
|
|
33
|
+
.positive()
|
|
34
|
+
.max(BUG_REPORT_AGENT_ACCESS_TTL_SECONDS)
|
|
35
|
+
.optional(),
|
|
36
|
+
}),
|
|
37
|
+
run: async (args) => {
|
|
38
|
+
const ownerEmail = getRequestUserEmail();
|
|
39
|
+
if (!ownerEmail) {
|
|
40
|
+
fail("Sign in to exchange an intake for agent access.", {
|
|
41
|
+
errorCode: "authentication_required",
|
|
42
|
+
statusCode: 401,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const organizationId = await getActiveOrganizationId();
|
|
46
|
+
const access = await requireOrganizationAccess(organizationId);
|
|
47
|
+
const session = await findOwnedClipIntakeSession(
|
|
48
|
+
args.intakeId,
|
|
49
|
+
access.email,
|
|
50
|
+
access.organizationId,
|
|
51
|
+
);
|
|
52
|
+
if (
|
|
53
|
+
!session ||
|
|
54
|
+
session.recordingId !== args.recordingId ||
|
|
55
|
+
session.status !== "completed"
|
|
56
|
+
) {
|
|
57
|
+
fail("This intake recording is not available.", {
|
|
58
|
+
errorCode: "intake_recording_unavailable",
|
|
59
|
+
statusCode: 404,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const [recording] = await getDb()
|
|
64
|
+
.select({
|
|
65
|
+
id: schema.recordings.id,
|
|
66
|
+
status: schema.recordings.status,
|
|
67
|
+
videoUrl: schema.recordings.videoUrl,
|
|
68
|
+
archivedAt: schema.recordings.archivedAt,
|
|
69
|
+
trashedAt: schema.recordings.trashedAt,
|
|
70
|
+
})
|
|
71
|
+
.from(schema.recordings)
|
|
72
|
+
.where(
|
|
73
|
+
and(
|
|
74
|
+
eq(schema.recordings.id, args.recordingId),
|
|
75
|
+
ownerEmailMatches(schema.recordings.ownerEmail, session.ownerEmail),
|
|
76
|
+
eq(schema.recordings.organizationId, session.organizationId),
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
.limit(1);
|
|
80
|
+
if (
|
|
81
|
+
!recording ||
|
|
82
|
+
recording.status !== "ready" ||
|
|
83
|
+
!recording.videoUrl ||
|
|
84
|
+
recording.archivedAt ||
|
|
85
|
+
recording.trashedAt
|
|
86
|
+
) {
|
|
87
|
+
fail("This intake recording is not available yet.", {
|
|
88
|
+
errorCode: "intake_recording_not_ready",
|
|
89
|
+
statusCode: 409,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const requestOrigin = getRequestContext()?.requestOrigin;
|
|
94
|
+
return runWithRequestContext(
|
|
95
|
+
{
|
|
96
|
+
userEmail: session.ownerEmail,
|
|
97
|
+
orgId: session.organizationId,
|
|
98
|
+
requestOrigin,
|
|
99
|
+
},
|
|
100
|
+
() =>
|
|
101
|
+
createRecordingAgentLink.run(
|
|
102
|
+
{
|
|
103
|
+
recordingId: args.recordingId,
|
|
104
|
+
ttlSeconds: args.ttlSeconds,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
caller: "http",
|
|
108
|
+
userEmail: session.ownerEmail,
|
|
109
|
+
orgId: session.organizationId,
|
|
110
|
+
},
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { defineAction, fail } from "@agent-native/core/action";
|
|
2
|
+
import { runWithRequestContext } from "@agent-native/core/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
abandonClipIntake,
|
|
7
|
+
attachClipIntakeRecording,
|
|
8
|
+
claimClipIntakeRecording,
|
|
9
|
+
findClipIntakeSession,
|
|
10
|
+
findRecoverableClipIntakeRecording,
|
|
11
|
+
releaseClipIntakeCreation,
|
|
12
|
+
} from "../server/lib/clip-intake.js";
|
|
13
|
+
import { getResumableSession } from "../server/lib/resumable-session.js";
|
|
14
|
+
import { BUG_REPORT_SEVERITIES } from "../shared/bug-report.js";
|
|
15
|
+
import { buildClipIntakeUrl } from "../shared/clip-intake.js";
|
|
16
|
+
import createRecording from "./create-recording.js";
|
|
17
|
+
import { createRecordingSchema } from "./lib/create-recording-schema.js";
|
|
18
|
+
import saveBugReportContext from "./save-bug-report-context.js";
|
|
19
|
+
import trashRecording from "./trash-recording.js";
|
|
20
|
+
|
|
21
|
+
const bugReportSchema = z.object({
|
|
22
|
+
projectId: z.string().max(120).nullish(),
|
|
23
|
+
title: z.string().max(500).nullish(),
|
|
24
|
+
description: z.string().max(5_000).nullish(),
|
|
25
|
+
severity: z.enum(BUG_REPORT_SEVERITIES).default("normal"),
|
|
26
|
+
sourceUrl: z.string().max(8_000).nullish(),
|
|
27
|
+
pageTitle: z.string().max(500).nullish(),
|
|
28
|
+
appVersion: z.string().max(120).nullish(),
|
|
29
|
+
environment: z.string().max(120).nullish(),
|
|
30
|
+
reporterEmail: z.string().max(320).nullish(),
|
|
31
|
+
reporterName: z.string().max(200).nullish(),
|
|
32
|
+
reporterId: z.string().max(200).nullish(),
|
|
33
|
+
metadata: z.record(z.string(), z.unknown()).nullish(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const intakeRecordingSchema = createRecordingSchema
|
|
37
|
+
.omit({ id: true, folderId: true, spaceIds: true, organizationId: true })
|
|
38
|
+
.extend({
|
|
39
|
+
intakeId: z.string().min(16).max(80),
|
|
40
|
+
intakeToken: z.string().min(1).max(4096),
|
|
41
|
+
bugReport: bugReportSchema.nullish(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function intakeUploadUrls(
|
|
45
|
+
intakeId: string,
|
|
46
|
+
intakeToken: string,
|
|
47
|
+
recordingId: string,
|
|
48
|
+
) {
|
|
49
|
+
return {
|
|
50
|
+
uploadChunkUrl: buildClipIntakeUrl("/api/clip-intake", {
|
|
51
|
+
recordingId,
|
|
52
|
+
operation: "chunk",
|
|
53
|
+
intakeId,
|
|
54
|
+
token: intakeToken,
|
|
55
|
+
}),
|
|
56
|
+
resetChunksUrl: buildClipIntakeUrl("/api/clip-intake", {
|
|
57
|
+
recordingId,
|
|
58
|
+
operation: "reset",
|
|
59
|
+
intakeId,
|
|
60
|
+
token: intakeToken,
|
|
61
|
+
}),
|
|
62
|
+
abortUrl: buildClipIntakeUrl("/api/clip-intake", {
|
|
63
|
+
recordingId,
|
|
64
|
+
operation: "abort",
|
|
65
|
+
intakeId,
|
|
66
|
+
token: intakeToken,
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default defineAction({
|
|
72
|
+
description:
|
|
73
|
+
"Create the one recording allowed by a signed Clips intake URL. This is a write-only anonymous capability and never grants library or agent read access.",
|
|
74
|
+
agentTool: false,
|
|
75
|
+
requiresAuth: false,
|
|
76
|
+
http: { method: "POST" },
|
|
77
|
+
maxBodyBytes: 64 * 1024,
|
|
78
|
+
schema: intakeRecordingSchema,
|
|
79
|
+
run: async (args) => {
|
|
80
|
+
const claimed = await claimClipIntakeRecording(
|
|
81
|
+
args.intakeId,
|
|
82
|
+
args.intakeToken,
|
|
83
|
+
);
|
|
84
|
+
if (!claimed) {
|
|
85
|
+
const existingSession = await findClipIntakeSession(
|
|
86
|
+
args.intakeId,
|
|
87
|
+
args.intakeToken,
|
|
88
|
+
);
|
|
89
|
+
const existing = existingSession
|
|
90
|
+
? await findRecoverableClipIntakeRecording(existingSession)
|
|
91
|
+
: null;
|
|
92
|
+
if (existing && existingSession) {
|
|
93
|
+
const resumableSession = await getResumableSession(existing.id);
|
|
94
|
+
return {
|
|
95
|
+
id: existing.id,
|
|
96
|
+
organizationId: existing.organizationId,
|
|
97
|
+
status: "uploading" as const,
|
|
98
|
+
uploadMode: resumableSession ? ("streaming" as const) : "buffered",
|
|
99
|
+
...intakeUploadUrls(args.intakeId, args.intakeToken, existing.id),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
fail("This intake URL has expired or has already been used.", {
|
|
103
|
+
errorCode: "intake_unavailable",
|
|
104
|
+
statusCode: 409,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { intakeId, intakeToken, bugReport, ...recordingArgs } = args;
|
|
109
|
+
let created: Awaited<ReturnType<typeof createRecording.run>>;
|
|
110
|
+
try {
|
|
111
|
+
created = await runWithRequestContext(
|
|
112
|
+
{
|
|
113
|
+
userEmail: claimed.ownerEmail,
|
|
114
|
+
orgId: claimed.organizationId,
|
|
115
|
+
},
|
|
116
|
+
() =>
|
|
117
|
+
createRecording.run(
|
|
118
|
+
{
|
|
119
|
+
...recordingArgs,
|
|
120
|
+
organizationId: claimed.organizationId,
|
|
121
|
+
visibility: "private",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
caller: "http",
|
|
125
|
+
userEmail: claimed.ownerEmail,
|
|
126
|
+
orgId: claimed.organizationId,
|
|
127
|
+
},
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
await releaseClipIntakeCreation(intakeId);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const cleanupCreatedRecording = async () => {
|
|
136
|
+
await abandonClipIntake(intakeId).catch((cleanupError: unknown) => {
|
|
137
|
+
console.warn("[clip-intake] session cleanup failed:", cleanupError);
|
|
138
|
+
});
|
|
139
|
+
await Promise.resolve(
|
|
140
|
+
runWithRequestContext(
|
|
141
|
+
{
|
|
142
|
+
userEmail: claimed.ownerEmail,
|
|
143
|
+
orgId: claimed.organizationId,
|
|
144
|
+
},
|
|
145
|
+
() =>
|
|
146
|
+
trashRecording.run(
|
|
147
|
+
{ id: created.id, skipIfReady: true },
|
|
148
|
+
{
|
|
149
|
+
caller: "http",
|
|
150
|
+
userEmail: claimed.ownerEmail,
|
|
151
|
+
orgId: claimed.organizationId,
|
|
152
|
+
},
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
).catch((cleanupError: unknown) => {
|
|
156
|
+
console.warn("[clip-intake] recording cleanup failed:", cleanupError);
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (bugReport) {
|
|
161
|
+
try {
|
|
162
|
+
await runWithRequestContext(
|
|
163
|
+
{
|
|
164
|
+
userEmail: claimed.ownerEmail,
|
|
165
|
+
orgId: claimed.organizationId,
|
|
166
|
+
},
|
|
167
|
+
() =>
|
|
168
|
+
saveBugReportContext.run(
|
|
169
|
+
{
|
|
170
|
+
recordingId: created.id,
|
|
171
|
+
...bugReport,
|
|
172
|
+
metadata: bugReport.metadata ?? undefined,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
caller: "http",
|
|
176
|
+
userEmail: claimed.ownerEmail,
|
|
177
|
+
orgId: claimed.organizationId,
|
|
178
|
+
},
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
} catch {
|
|
182
|
+
await cleanupCreatedRecording();
|
|
183
|
+
fail(
|
|
184
|
+
"Could not save the bug report context. This intake was not completed. Try again.",
|
|
185
|
+
{
|
|
186
|
+
errorCode: "intake_context_unavailable",
|
|
187
|
+
statusCode: 503,
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await attachClipIntakeRecording(intakeId, created.id);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
await cleanupCreatedRecording();
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
id: created.id,
|
|
202
|
+
organizationId: created.organizationId,
|
|
203
|
+
status: created.status,
|
|
204
|
+
uploadMode: created.uploadMode,
|
|
205
|
+
...intakeUploadUrls(intakeId, intakeToken, created.id),
|
|
206
|
+
};
|
|
207
|
+
},
|
|
208
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core/action";
|
|
2
|
+
import {
|
|
3
|
+
buildAgentAccessUrl,
|
|
4
|
+
createScopedAgentAccessGrant,
|
|
5
|
+
getAppProductionUrl,
|
|
6
|
+
getRequestContext,
|
|
7
|
+
getRequestUserEmail,
|
|
8
|
+
} from "@agent-native/core/server";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
12
|
+
import { getServerAppBasePath } from "../server/lib/public-agent-context.js";
|
|
13
|
+
import {
|
|
14
|
+
getActiveOrganizationId,
|
|
15
|
+
nanoid,
|
|
16
|
+
requireOrganizationAccess,
|
|
17
|
+
} from "../server/lib/recordings.js";
|
|
18
|
+
import {
|
|
19
|
+
CLIP_INTAKE_DEFAULT_TTL_SECONDS,
|
|
20
|
+
CLIP_INTAKE_ID_PARAM,
|
|
21
|
+
CLIP_INTAKE_MAX_TTL_SECONDS,
|
|
22
|
+
CLIP_INTAKE_RESOURCE_KIND,
|
|
23
|
+
CLIP_INTAKE_TOKEN_PARAM,
|
|
24
|
+
} from "../shared/clip-intake.js";
|
|
25
|
+
|
|
26
|
+
function appOrigin(): string {
|
|
27
|
+
const origin = getRequestContext()?.requestOrigin || getAppProductionUrl();
|
|
28
|
+
try {
|
|
29
|
+
return new URL(origin).origin;
|
|
30
|
+
} catch {
|
|
31
|
+
return "http://localhost:3000";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default defineAction({
|
|
36
|
+
description:
|
|
37
|
+
"Create a short-lived, one-recording Clips intake URL. An anonymous visitor can use it to submit one private recording, but the URL cannot read the Clips library or call agent APIs.",
|
|
38
|
+
agentTool: false,
|
|
39
|
+
schema: z.object({
|
|
40
|
+
ttlSeconds: z
|
|
41
|
+
.number()
|
|
42
|
+
.int()
|
|
43
|
+
.min(60)
|
|
44
|
+
.max(CLIP_INTAKE_MAX_TTL_SECONDS)
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Intake URL lifetime in seconds. Defaults to one hour."),
|
|
47
|
+
}),
|
|
48
|
+
run: async (args) => {
|
|
49
|
+
const ownerEmail = getRequestUserEmail();
|
|
50
|
+
if (!ownerEmail) throw new Error("Sign in to create an intake URL.");
|
|
51
|
+
const organizationId = await getActiveOrganizationId();
|
|
52
|
+
const access = await requireOrganizationAccess(organizationId);
|
|
53
|
+
const intakeId = nanoid(24);
|
|
54
|
+
const ttlSeconds = args.ttlSeconds ?? CLIP_INTAKE_DEFAULT_TTL_SECONDS;
|
|
55
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
56
|
+
|
|
57
|
+
await getDb().insert(schema.clipIntakeSessions).values({
|
|
58
|
+
id: intakeId,
|
|
59
|
+
ownerEmail: access.email,
|
|
60
|
+
organizationId: access.organizationId,
|
|
61
|
+
status: "open",
|
|
62
|
+
expiresAt,
|
|
63
|
+
createdAt: new Date().toISOString(),
|
|
64
|
+
updatedAt: new Date().toISOString(),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const grant = createScopedAgentAccessGrant({
|
|
68
|
+
resourceKind: CLIP_INTAKE_RESOURCE_KIND,
|
|
69
|
+
resourceId: intakeId,
|
|
70
|
+
ttlSeconds,
|
|
71
|
+
});
|
|
72
|
+
const intakePath = `/bug-report?${new URLSearchParams({
|
|
73
|
+
[CLIP_INTAKE_ID_PARAM]: intakeId,
|
|
74
|
+
}).toString()}`;
|
|
75
|
+
const url = buildAgentAccessUrl({
|
|
76
|
+
path: intakePath,
|
|
77
|
+
origin: appOrigin(),
|
|
78
|
+
basePath: getServerAppBasePath(),
|
|
79
|
+
token: grant.token,
|
|
80
|
+
tokenParam: CLIP_INTAKE_TOKEN_PARAM,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return { intakeId, url, expiresAt: grant.expiresAt, ttlSeconds };
|
|
84
|
+
},
|
|
85
|
+
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type BugReportContext,
|
|
6
6
|
type BugReportSeverity,
|
|
7
7
|
} from "@shared/bug-report";
|
|
8
|
+
import type { ClipIntakeParams } from "@shared/clip-intake";
|
|
8
9
|
import { IconBug, IconShieldCheck } from "@tabler/icons-react";
|
|
9
10
|
import { useEffect, useState } from "react";
|
|
10
11
|
|
|
@@ -24,6 +25,7 @@ import { cn } from "@/lib/utils";
|
|
|
24
25
|
interface BugReportFormProps {
|
|
25
26
|
className?: string;
|
|
26
27
|
initialContext?: BugReportContext | null;
|
|
28
|
+
intake?: ClipIntakeParams | null;
|
|
27
29
|
onRecorderOpened?: (recorderWindow: Window | null) => void;
|
|
28
30
|
onRecordingStarted?: () => void;
|
|
29
31
|
}
|
|
@@ -45,6 +47,7 @@ function openRecorder(url: string): Window | null {
|
|
|
45
47
|
export function BugReportForm({
|
|
46
48
|
className,
|
|
47
49
|
initialContext,
|
|
50
|
+
intake,
|
|
48
51
|
onRecorderOpened,
|
|
49
52
|
onRecordingStarted,
|
|
50
53
|
}: BugReportFormProps) {
|
|
@@ -91,6 +94,10 @@ export function BugReportForm({
|
|
|
91
94
|
params.set("intent", "bug-report");
|
|
92
95
|
params.set("mode", "screen");
|
|
93
96
|
params.set("surface", "browser");
|
|
97
|
+
if (intake) {
|
|
98
|
+
params.set("clip_intake_id", intake.intakeId);
|
|
99
|
+
params.set("clip_intake", intake.token);
|
|
100
|
+
}
|
|
94
101
|
const recorderWindow = openRecorder(
|
|
95
102
|
`${appBasePath()}/record?${params.toString()}`,
|
|
96
103
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { trackEvent } from "@agent-native/core/client/analytics";
|
|
2
2
|
import { captureClientException } from "@agent-native/core/client/analytics";
|
|
3
3
|
import { appBasePath } from "@agent-native/core/client/api-path";
|
|
4
|
+
import { redactBrowserDiagnosticString } from "@shared/browser-diagnostics";
|
|
4
5
|
import { waitForAcceptedRecordingAfterFinalizeError } from "@shared/finalize-recovery";
|
|
5
6
|
import {
|
|
6
7
|
chooseFallbackAudioInput,
|
|
@@ -140,6 +141,8 @@ export interface RecorderEngineOptions {
|
|
|
140
141
|
uploadUrl?: string;
|
|
141
142
|
/** Abort URL. Default `/api/uploads/:id/abort`. */
|
|
142
143
|
abortUrl?: string;
|
|
144
|
+
/** Reset-chunks URL. Defaults to the authenticated recording route. */
|
|
145
|
+
resetUrl?: string;
|
|
143
146
|
/**
|
|
144
147
|
* Upload strategy returned by create-recording.
|
|
145
148
|
* `"streaming"` — server has a resumable session; engine flushes aligned
|
|
@@ -505,7 +508,10 @@ function fetchAbortError(signal: AbortSignal, err: unknown): Error {
|
|
|
505
508
|
|
|
506
509
|
export class RecorderEngine {
|
|
507
510
|
readonly opts: Required<
|
|
508
|
-
Pick<
|
|
511
|
+
Pick<
|
|
512
|
+
RecorderEngineOptions,
|
|
513
|
+
"chunkIntervalMs" | "uploadUrl" | "abortUrl" | "resetUrl"
|
|
514
|
+
>
|
|
509
515
|
> &
|
|
510
516
|
RecorderEngineOptions;
|
|
511
517
|
|
|
@@ -617,6 +623,11 @@ export class RecorderEngine {
|
|
|
617
623
|
abortUrl:
|
|
618
624
|
options.abortUrl ??
|
|
619
625
|
`${appBasePath()}/api/uploads/${options.recordingId}/abort`,
|
|
626
|
+
resetUrl:
|
|
627
|
+
options.resetUrl ??
|
|
628
|
+
(options.uploadUrl
|
|
629
|
+
? options.uploadUrl.replace(/\/chunk(?:\?.*)?$/, "/reset-chunks")
|
|
630
|
+
: `${appBasePath()}/api/uploads/${options.recordingId}/reset-chunks`),
|
|
620
631
|
...options,
|
|
621
632
|
};
|
|
622
633
|
}
|
|
@@ -1149,11 +1160,17 @@ export class RecorderEngine {
|
|
|
1149
1160
|
recordingId: string;
|
|
1150
1161
|
uploadUrl: string;
|
|
1151
1162
|
abortUrl: string;
|
|
1163
|
+
resetUrl?: string;
|
|
1152
1164
|
uploadMode?: UploadMode;
|
|
1153
1165
|
}): void {
|
|
1154
1166
|
this.opts.recordingId = target.recordingId;
|
|
1155
1167
|
this.opts.uploadUrl = target.uploadUrl;
|
|
1156
1168
|
this.opts.abortUrl = target.abortUrl;
|
|
1169
|
+
this.opts.resetUrl =
|
|
1170
|
+
target.resetUrl ??
|
|
1171
|
+
(target.uploadUrl.endsWith("/chunk")
|
|
1172
|
+
? target.uploadUrl.slice(0, -"/chunk".length) + "/reset-chunks"
|
|
1173
|
+
: `${appBasePath()}/api/uploads/${target.recordingId}/reset-chunks`);
|
|
1157
1174
|
this.opts.uploadMode = target.uploadMode ?? "buffered";
|
|
1158
1175
|
this.uploadGenerationId = null;
|
|
1159
1176
|
}
|
|
@@ -1614,9 +1631,7 @@ export class RecorderEngine {
|
|
|
1614
1631
|
compression: CompressionUploadMeta | null,
|
|
1615
1632
|
signal?: AbortSignal,
|
|
1616
1633
|
): Promise<UploadMode> {
|
|
1617
|
-
const resetUrl =
|
|
1618
|
-
this.opts.recordingId
|
|
1619
|
-
}/reset-chunks`;
|
|
1634
|
+
const resetUrl = this.opts.resetUrl;
|
|
1620
1635
|
const uploadMimeType = compression?.outputMimeType || this.mimeType;
|
|
1621
1636
|
let resetRes: Response;
|
|
1622
1637
|
try {
|
|
@@ -2404,7 +2419,9 @@ export class RecorderEngine {
|
|
|
2404
2419
|
httpStatus: String(res.status),
|
|
2405
2420
|
},
|
|
2406
2421
|
extra: {
|
|
2407
|
-
url,
|
|
2422
|
+
url: redactBrowserDiagnosticString(url, {
|
|
2423
|
+
redactQueryValues: true,
|
|
2424
|
+
}),
|
|
2408
2425
|
status: res.status,
|
|
2409
2426
|
statusText: res.statusText,
|
|
2410
2427
|
responseBodyTail: text?.slice(0, 2000) ?? "",
|
|
@@ -53,10 +53,11 @@ export async function fetchVideoStorageStatus(): Promise<VideoStorageStatus> {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export function useVideoStorageStatus() {
|
|
56
|
+
export function useVideoStorageStatus(enabled = true) {
|
|
57
57
|
return useQuery({
|
|
58
58
|
queryKey: VIDEO_STORAGE_STATUS_KEY,
|
|
59
59
|
queryFn: fetchVideoStorageStatus,
|
|
60
|
+
enabled,
|
|
60
61
|
staleTime: 60_000,
|
|
61
62
|
});
|
|
62
63
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createBugReportSubmissionMessage,
|
|
9
9
|
type BugReportAgentLink,
|
|
10
10
|
} from "@shared/bug-report";
|
|
11
|
+
import { parseClipIntakeParams } from "@shared/clip-intake";
|
|
11
12
|
import {
|
|
12
13
|
IconArrowLeft,
|
|
13
14
|
IconCheck,
|
|
@@ -43,6 +44,7 @@ export default function BugReportDoneRoute() {
|
|
|
43
44
|
);
|
|
44
45
|
const recordingId = params.get("recordingId")?.trim() || null;
|
|
45
46
|
const returnUrl = params.get("returnUrl")?.trim() || null;
|
|
47
|
+
const intake = useMemo(() => parseClipIntakeParams(params), [params]);
|
|
46
48
|
|
|
47
49
|
const recordingUrl = recordingId
|
|
48
50
|
? absoluteAppUrl(`/r/${encodeURIComponent(recordingId)}`)
|
|
@@ -55,19 +57,19 @@ export default function BugReportDoneRoute() {
|
|
|
55
57
|
let cancelled = false;
|
|
56
58
|
void (async () => {
|
|
57
59
|
let agentLink: BugReportAgentLink | null = null;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"create-recording-agent-link"
|
|
61
|
-
{
|
|
60
|
+
if (!intake) {
|
|
61
|
+
try {
|
|
62
|
+
agentLink = (await callAction("create-recording-agent-link", {
|
|
62
63
|
recordingId,
|
|
63
64
|
ttlSeconds: BUG_REPORT_AGENT_ACCESS_TTL_SECONDS,
|
|
64
|
-
} as
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
})) as BugReportAgentLink;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// The completion message keeps access explicitly unavailable when
|
|
68
|
+
// the authenticated exchange cannot be completed.
|
|
69
|
+
console.warn("[bug-report] agent link unavailable:", error);
|
|
70
|
+
}
|
|
68
71
|
}
|
|
69
72
|
if (cancelled) return;
|
|
70
|
-
|
|
71
73
|
const message = createBugReportSubmissionMessage({
|
|
72
74
|
recordingId,
|
|
73
75
|
recordingUrl,
|
|
@@ -88,7 +90,7 @@ export default function BugReportDoneRoute() {
|
|
|
88
90
|
return () => {
|
|
89
91
|
cancelled = true;
|
|
90
92
|
};
|
|
91
|
-
}, [embedUrl, recordingId, recordingUrl, returnUrl]);
|
|
93
|
+
}, [embedUrl, intake, recordingId, recordingUrl, returnUrl]);
|
|
92
94
|
|
|
93
95
|
const copyRecordingUrl = async () => {
|
|
94
96
|
if (!recordingUrl) return;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
isBugReportSubmissionMessage,
|
|
4
4
|
parseBugReportContext,
|
|
5
5
|
} from "@shared/bug-report";
|
|
6
|
+
import { parseClipIntakeParams } from "@shared/clip-intake";
|
|
6
7
|
import { useEffect, useMemo, useRef } from "react";
|
|
7
8
|
import { useLocation, useOutlet } from "react-router";
|
|
8
9
|
|
|
@@ -33,6 +34,10 @@ export default function BugReportRoute() {
|
|
|
33
34
|
const params = new URLSearchParams(location.search);
|
|
34
35
|
return parseBugReportContext(params, { allowLoose: true });
|
|
35
36
|
}, [location.search]);
|
|
37
|
+
const intake = useMemo(
|
|
38
|
+
() => parseClipIntakeParams(new URLSearchParams(location.search)),
|
|
39
|
+
[location.search],
|
|
40
|
+
);
|
|
36
41
|
|
|
37
42
|
const recorderWindowRef = useRef<Window | null>(null);
|
|
38
43
|
const hostOrigin = originFor(
|
|
@@ -66,6 +71,7 @@ export default function BugReportRoute() {
|
|
|
66
71
|
<section className="rounded-lg border bg-card p-4 shadow-sm sm:p-5">
|
|
67
72
|
<BugReportForm
|
|
68
73
|
initialContext={initialContext}
|
|
74
|
+
intake={intake}
|
|
69
75
|
onRecorderOpened={(recorderWindow) => {
|
|
70
76
|
recorderWindowRef.current = recorderWindow;
|
|
71
77
|
}}
|