@agent-native/core 0.196.0-nightly-20260928172902 → 0.196.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +17 -0
- package/corpus/templates/assets/CHANGELOG.md +18 -0
- package/corpus/templates/brain/CHANGELOG.md +6 -0
- package/corpus/templates/calendar/CHANGELOG.md +4 -0
- package/corpus/templates/clips/CHANGELOG.md +19 -0
- package/corpus/templates/clips/actions/create-recording-agent-link.ts +8 -2
- package/corpus/templates/clips/actions/update-recording.ts +5 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +10 -3
- package/corpus/templates/clips/changelog/2026-09-28-agent-links-stay-valid-when-clip-details-change.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-28-password-changes-immediately-invalidate-old-recording-links.md +6 -0
- package/corpus/templates/clips/changelog/2026-09-28-password-protected-recording-links-stop-working-when-the-pas.md +6 -0
- package/corpus/templates/clips/server/db/schema.ts +3 -0
- package/corpus/templates/clips/server/lib/player-video-url.ts +9 -1
- package/corpus/templates/clips/server/lib/public-agent-context.ts +12 -4
- package/corpus/templates/clips/server/lib/share-password.ts +25 -0
- package/corpus/templates/clips/server/plugins/db.ts +36 -0
- package/corpus/templates/clips/server/routes/[...page].get.ts +8 -1
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +33 -6
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +34 -6
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +26 -5
- package/corpus/templates/content/CHANGELOG.md +1 -0
- package/corpus/templates/crm/CHANGELOG.md +12 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +265 -3
- package/corpus/templates/design/CHANGELOG.md +21 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +463 -6
- package/corpus/templates/design/app/pages/Index.tsx +1 -1
- package/corpus/templates/design/changelog/2026-09-27-auto-focused-app-fields-no-longer-interrupt-canvas-shortcuts.md +6 -0
- package/corpus/templates/design/changelog/2026-09-28-model-picker-loads-available-models.md +6 -0
- package/corpus/templates/design/changelog/2026-09-28-template-menus-stay-available-on-hover.md +6 -0
- package/corpus/templates/dispatch/CHANGELOG.md +18 -0
- package/corpus/templates/forms/CHANGELOG.md +6 -0
- package/corpus/templates/mail/.env.example +1 -1
- package/corpus/templates/mail/CHANGELOG.md +33 -0
- package/corpus/templates/mail/changelog/2026-09-28-fixed-inbox-loading-stalls-and-showed-progress-during-google-2.md +6 -0
- package/corpus/templates/mail/changelog/2026-09-28-fixed-inbox-loading-stalls-and-showed-progress-during-google.md +6 -0
- package/corpus/templates/mail/server/db/schema.ts +7 -0
- package/corpus/templates/mail/server/lib/ai-filter-backfill.ts +4 -0
- package/corpus/templates/mail/server/lib/jobs.ts +5 -2
- package/corpus/templates/mail/server/plugins/db.ts +16 -0
- package/corpus/templates/mail/server/plugins/mail-jobs.ts +355 -44
- package/corpus/templates/slides/CHANGELOG.md +28 -0
- package/corpus/templates/slides/app/pages/Index.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-09-28-model-picker-loads-available-models.md +6 -0
- package/corpus/templates/slides/changelog/2026-09-28-template-menus-stay-available-on-hover.md +6 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/client/auth/AuthPage.js +3 -2
- package/dist/client/org/OrgSwitcher.js +31 -19
- package/dist/client/settings/UsageSection.js +3 -3
- package/dist/client/use-action.js +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/jobs/background-automation-runner.d.ts +2 -0
- package/dist/jobs/background-automation-runner.js +20 -1
- package/dist/jobs/sweep-hooks.d.ts +6 -2
- package/dist/jobs/sweep-hooks.js +17 -9
- package/dist/localization/core-messages/ar-SA.js +2 -2
- package/dist/localization/core-messages/de-DE.js +2 -2
- package/dist/localization/core-messages/en-US.d.ts +2 -2
- package/dist/localization/core-messages/en-US.js +2 -2
- package/dist/localization/core-messages/es-ES.js +2 -2
- package/dist/localization/core-messages/fr-FR.js +2 -2
- package/dist/localization/core-messages/hi-IN.js +2 -2
- package/dist/localization/core-messages/ja-JP.js +2 -2
- package/dist/localization/core-messages/ko-KR.js +2 -2
- package/dist/localization/core-messages/pt-BR.js +2 -2
- package/dist/localization/core-messages/zh-CN.js +2 -2
- package/dist/localization/core-messages/zh-TW.js +2 -2
- package/dist/localization/core-messages.d.ts +2 -2
- package/dist/notifications/routes.d.ts +5 -5
- package/dist/observability/routes.d.ts +6 -6
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +4 -4
- package/dist/server/agent-chat-plugin.js +6 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-default-model-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/triggers/dispatcher.js +167 -36
- package/dist/triggers/event-queue.d.ts +18 -1
- package/dist/triggers/event-queue.js +143 -10
- package/package.json +4 -4
package/corpus/README.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable user-facing changes to Agent-Native Analytics 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-28
|
|
7
|
+
|
|
8
|
+
### Improved
|
|
9
|
+
|
|
10
|
+
- Ask Analytics now starts with a focused title.
|
|
11
|
+
|
|
12
|
+
## 2026-09-27
|
|
13
|
+
|
|
14
|
+
### Improved
|
|
15
|
+
|
|
16
|
+
- Ask Analytics now opens with a centered chat and starter guidance.
|
|
17
|
+
- Metric cards include their comparison period and change.
|
|
18
|
+
- Complete single-number analysis queries show a compact card, and sampled tables are marked.
|
|
19
|
+
- The chat home keeps suggested prompts above the composer in a centered layout.
|
|
20
|
+
|
|
6
21
|
## 2026-09-26
|
|
7
22
|
|
|
8
23
|
### Improved
|
|
@@ -24,6 +39,8 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
24
39
|
|
|
25
40
|
### Improved
|
|
26
41
|
|
|
42
|
+
- In the redesigned Settings, alert rules and data sources are tabs on Analytics General, and error emails and the bell sound have their own Notifications page.
|
|
43
|
+
- Settings now opens from your name at the bottom of the sidebar, or with ⌘,.
|
|
27
44
|
- Analytics captures confirmed query guidance for future conversations
|
|
28
45
|
- Public status pages, dashboards, and analyses show resource details in link previews
|
|
29
46
|
- The chat composer keeps a consistent background while AI setup is open.
|
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable user-facing changes to Assets 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-28
|
|
7
|
+
|
|
8
|
+
### Improved
|
|
9
|
+
|
|
10
|
+
- Create starter prompts now appear below the composer.
|
|
11
|
+
|
|
12
|
+
## 2026-09-27
|
|
13
|
+
|
|
14
|
+
### Improved
|
|
15
|
+
|
|
16
|
+
- Assets chat now opens with a centered composer and starter prompts.
|
|
17
|
+
- Reference actions use clearer, shorter labels.
|
|
18
|
+
- Show generated image variations with save, reference, and refine actions in chat
|
|
19
|
+
- The Create chat home keeps suggested prompts above the composer in a centered layout.
|
|
20
|
+
|
|
6
21
|
## 2026-09-26
|
|
7
22
|
|
|
8
23
|
### Improved
|
|
@@ -22,6 +37,9 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
22
37
|
|
|
23
38
|
### Improved
|
|
24
39
|
|
|
40
|
+
- Image generation uses the provider your organization picks for it, then falls back to Builder.io
|
|
41
|
+
- In the new Settings, generation setup and storage are on Assets › General, and generation emails are on Assets › Notifications.
|
|
42
|
+
- Settings now opens from your name at the bottom of the sidebar, or with ⌘,.
|
|
25
43
|
- Brand-kit and template uploads show how to connect file storage when it is not configured
|
|
26
44
|
- Public asset library links show their title and description in previews.
|
|
27
45
|
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable user-facing changes to Agent-Native Brain 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-25
|
|
7
|
+
|
|
8
|
+
### Improved
|
|
9
|
+
|
|
10
|
+
- Brain settings now live in tabs on Brain › General (Identity, Behavior, Publishing, Safety, and Privacy), and each change saves as you make it.
|
|
11
|
+
|
|
6
12
|
## 2026-09-22
|
|
7
13
|
|
|
8
14
|
### Added
|
|
@@ -7,6 +7,8 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
7
7
|
|
|
8
8
|
### Improved
|
|
9
9
|
|
|
10
|
+
- Calendar time suggestions now format dates and times in your language.
|
|
11
|
+
- Invitation rules now link to Automations for actions beyond accept, decline, and hide.
|
|
10
12
|
- Events created by the agent now show their date and time with a direct link to Calendar.
|
|
11
13
|
- Calendar shows cards for event changes and lets you start an event draft from a suggested time.
|
|
12
14
|
|
|
@@ -50,6 +52,8 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
50
52
|
|
|
51
53
|
### Improved
|
|
52
54
|
|
|
55
|
+
- Calendar settings are reorganized into General, Calendars, Booking, and Notifications in the new Settings.
|
|
56
|
+
- Settings now opens from your name at the bottom of the sidebar, or with ⌘,.
|
|
53
57
|
- The bookings list shows the latest bookings first and keeps dates and times together.
|
|
54
58
|
- Colleague events use the saved accent color shown in the calendar sidebar.
|
|
55
59
|
- If Zoom cannot confirm a meeting, Calendar keeps the booking and follows up with meeting details
|
|
@@ -3,6 +3,21 @@
|
|
|
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-28
|
|
7
|
+
|
|
8
|
+
### Improved
|
|
9
|
+
|
|
10
|
+
- Viewers can share recordings with agents from shared recording menus
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Agent links stay valid when clip details change
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
|
|
18
|
+
- Password changes immediately invalidate old recording links
|
|
19
|
+
- Password-protected recording links stop working when the password changes
|
|
20
|
+
|
|
6
21
|
## 2026-09-26
|
|
7
22
|
|
|
8
23
|
### Improved
|
|
@@ -16,6 +31,7 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
16
31
|
|
|
17
32
|
### Fixed
|
|
18
33
|
|
|
34
|
+
- Removing a member from your organization now completes instead of failing with a pending cleanup error
|
|
19
35
|
- Clips asks you to connect storage only when you choose an upload
|
|
20
36
|
- Clips can play recordings from public S3-compatible storage in more hosted environments.
|
|
21
37
|
- Dismissing storage setup now cancels queued uploads.
|
|
@@ -31,6 +47,9 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
31
47
|
|
|
32
48
|
### Improved
|
|
33
49
|
|
|
50
|
+
- Clips settings are reorganized in the new Settings: General with Recordings and Meetings tabs, Notifications, and Slack link previews under Channels.
|
|
51
|
+
- The account menu at the bottom of the sidebar shows your photo, name, and organization, and holds Settings, Usage, Get apps and extensions, and Log out
|
|
52
|
+
- Video storage now uses the shared storage form, and Clear credentials asks before it removes your storage keys
|
|
34
53
|
- Connect Builder storage by creating an account in one click.
|
|
35
54
|
- Public clip embeds and meeting notes show richer link previews.
|
|
36
55
|
- The no-comments sidebar gives viewers a concise reason to try Clips and a clear path to sign up.
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
CLIPS_AGENT_ACCESS_TTL_SECONDS,
|
|
13
13
|
getServerAppBasePath,
|
|
14
14
|
} from "../server/lib/public-agent-context.js";
|
|
15
|
+
import { getRecordingAccessTokenResourceId } from "../server/lib/share-password.js";
|
|
15
16
|
import {
|
|
16
17
|
buildAgentApiUrls,
|
|
17
18
|
CLIP_AGENT_ACCESS_TOKEN_PREFIX,
|
|
@@ -64,7 +65,8 @@ export default defineAction({
|
|
|
64
65
|
|
|
65
66
|
const recording = access.resource as {
|
|
66
67
|
id: string;
|
|
67
|
-
password
|
|
68
|
+
password: string | null;
|
|
69
|
+
sharePasswordVersion: string;
|
|
68
70
|
archivedAt?: string | null;
|
|
69
71
|
trashedAt?: string | null;
|
|
70
72
|
};
|
|
@@ -86,7 +88,11 @@ export default defineAction({
|
|
|
86
88
|
|
|
87
89
|
const grant = createScopedAgentAccessGrant({
|
|
88
90
|
resourceKind: CLIP_AGENT_ACCESS_TOKEN_PREFIX,
|
|
89
|
-
resourceId:
|
|
91
|
+
resourceId: getRecordingAccessTokenResourceId(
|
|
92
|
+
recording.id,
|
|
93
|
+
recording.password,
|
|
94
|
+
recording.sharePasswordVersion,
|
|
95
|
+
),
|
|
90
96
|
viewerEmail: getRequestUserEmail() || undefined,
|
|
91
97
|
agentLabel: args.agentLabel,
|
|
92
98
|
ttlSeconds: args.ttlSeconds ?? CLIPS_AGENT_ACCESS_TTL_SECONDS,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
1
3
|
import { defineAction } from "@agent-native/core/action";
|
|
2
4
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
3
5
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
@@ -64,8 +66,10 @@ export default defineAction({
|
|
|
64
66
|
patch.defaultSpeed = args.defaultSpeed;
|
|
65
67
|
if (typeof args.animatedThumbnailEnabled === "boolean")
|
|
66
68
|
patch.animatedThumbnailEnabled = args.animatedThumbnailEnabled;
|
|
67
|
-
if (args.password !== undefined)
|
|
69
|
+
if (args.password !== undefined) {
|
|
68
70
|
patch.password = encryptSharePassword(args.password);
|
|
71
|
+
patch.sharePasswordVersion = randomUUID();
|
|
72
|
+
}
|
|
69
73
|
if (args.expiresAt !== undefined) patch.expiresAt = args.expiresAt ?? null;
|
|
70
74
|
if (typeof args.chaptersJson === "string")
|
|
71
75
|
patch.chaptersJson = args.chaptersJson;
|
|
@@ -251,9 +251,11 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
|
|
|
251
251
|
verifyScopedAgentAccessToken,
|
|
252
252
|
},
|
|
253
253
|
{ resolveAccess },
|
|
254
|
+
{ getRecordingAccessTokenResourceId },
|
|
254
255
|
] = await Promise.all([
|
|
255
256
|
import("@agent-native/core/server"),
|
|
256
257
|
import("@agent-native/core/sharing"),
|
|
258
|
+
import("../../server/lib/share-password.js"),
|
|
257
259
|
]);
|
|
258
260
|
|
|
259
261
|
const db = getDb();
|
|
@@ -265,6 +267,7 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
|
|
|
265
267
|
thumbnailUrl: schema.recordings.thumbnailUrl,
|
|
266
268
|
animatedThumbnailUrl: schema.recordings.animatedThumbnailUrl,
|
|
267
269
|
updatedAt: schema.recordings.updatedAt,
|
|
270
|
+
sharePasswordVersion: schema.recordings.sharePasswordVersion,
|
|
268
271
|
visibility: schema.recordings.visibility,
|
|
269
272
|
status: schema.recordings.status,
|
|
270
273
|
ownerEmail: schema.recordings.ownerEmail,
|
|
@@ -282,15 +285,19 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
|
|
|
282
285
|
|
|
283
286
|
const agentAccessToken = url.searchParams.get(CLIPS_AGENT_ACCESS_PARAM) ?? "";
|
|
284
287
|
const hasAgentAccessToken = Boolean(agentAccessToken);
|
|
288
|
+
if (!rec) return shareLoaderData(emptyLoaderData(url), hasAgentAccessToken);
|
|
289
|
+
|
|
285
290
|
const tokenGrantsAgentAccess = agentAccessToken
|
|
286
291
|
? verifyScopedAgentAccessToken(agentAccessToken, {
|
|
287
292
|
resourceKind: CLIP_AGENT_ACCESS_TOKEN_PREFIX,
|
|
288
|
-
resourceId:
|
|
293
|
+
resourceId: getRecordingAccessTokenResourceId(
|
|
294
|
+
id,
|
|
295
|
+
rec.password,
|
|
296
|
+
rec.sharePasswordVersion,
|
|
297
|
+
),
|
|
289
298
|
}).ok
|
|
290
299
|
: false;
|
|
291
300
|
|
|
292
|
-
if (!rec) return shareLoaderData(emptyLoaderData(url), hasAgentAccessToken);
|
|
293
|
-
|
|
294
301
|
if (isRecordingExpired(rec.expiresAt)) {
|
|
295
302
|
return shareLoaderData(emptyLoaderData(url), hasAgentAccessToken);
|
|
296
303
|
}
|
|
@@ -171,6 +171,9 @@ export const recordings = table("recordings", {
|
|
|
171
171
|
chaptersJson: text("chapters_json").notNull().default("[]"),
|
|
172
172
|
|
|
173
173
|
password: text("password"),
|
|
174
|
+
sharePasswordVersion: text("share_password_version")
|
|
175
|
+
.notNull()
|
|
176
|
+
.default("initial"),
|
|
174
177
|
expiresAt: text("expires_at"),
|
|
175
178
|
|
|
176
179
|
enableComments: boolean("enable_comments").notNull().default(true),
|
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
isLoomEmbedBackedRecording,
|
|
6
6
|
isLoomRecordingSource,
|
|
7
7
|
} from "../../shared/loom.js";
|
|
8
|
+
import { getRecordingAccessTokenResourceId } from "./share-password.js";
|
|
8
9
|
|
|
9
10
|
type PlayerVideoRecording = {
|
|
10
11
|
id: string;
|
|
11
12
|
password?: string | null;
|
|
13
|
+
sharePasswordVersion?: string | null;
|
|
12
14
|
sourceAppName?: string | null;
|
|
13
15
|
sourceWindowTitle?: string | null;
|
|
14
16
|
videoUrl?: string | null;
|
|
@@ -66,7 +68,13 @@ export function resolvePlayerVideoUrl(
|
|
|
66
68
|
recording.password &&
|
|
67
69
|
resolvedVideoUrl.startsWith("/api/video/")
|
|
68
70
|
) {
|
|
69
|
-
const token = signShortLivedToken({
|
|
71
|
+
const token = signShortLivedToken({
|
|
72
|
+
resourceId: getRecordingAccessTokenResourceId(
|
|
73
|
+
recording.id,
|
|
74
|
+
recording.password,
|
|
75
|
+
recording.sharePasswordVersion,
|
|
76
|
+
),
|
|
77
|
+
});
|
|
70
78
|
resolvedVideoUrl = appendQueryParam(resolvedVideoUrl, "t", token);
|
|
71
79
|
}
|
|
72
80
|
|
|
@@ -40,7 +40,10 @@ import {
|
|
|
40
40
|
import { resolveTranscriptPresentation } from "../../shared/transcript-status.js";
|
|
41
41
|
import { getDb, schema } from "../db/index.js";
|
|
42
42
|
import { recordAgentView } from "./agent-views.js";
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
getRecordingAccessTokenResourceId,
|
|
45
|
+
verifySharePassword,
|
|
46
|
+
} from "./share-password.js";
|
|
44
47
|
|
|
45
48
|
export type PublicAgentRecording = typeof schema.recordings._.inferSelect;
|
|
46
49
|
export type PublicAgentTranscript =
|
|
@@ -302,11 +305,16 @@ export async function loadPublicAgentAccess(
|
|
|
302
305
|
const viewerIsOwner = Boolean(
|
|
303
306
|
session?.email && sameOwnerEmail(session.email, recording.ownerEmail),
|
|
304
307
|
);
|
|
308
|
+
const scopedRecordingId = getRecordingAccessTokenResourceId(
|
|
309
|
+
recording.id,
|
|
310
|
+
recording.password,
|
|
311
|
+
recording.sharePasswordVersion,
|
|
312
|
+
);
|
|
305
313
|
const suppliedToken = options.token ?? "";
|
|
306
314
|
const tokenAccess = suppliedToken
|
|
307
315
|
? verifyScopedAgentAccessToken(suppliedToken, {
|
|
308
316
|
resourceKind: CLIP_AGENT_ACCESS_TOKEN_PREFIX,
|
|
309
|
-
resourceId:
|
|
317
|
+
resourceId: scopedRecordingId,
|
|
310
318
|
})
|
|
311
319
|
: null;
|
|
312
320
|
const tokenAllowsAgentAccess = Boolean(tokenAccess?.ok);
|
|
@@ -346,7 +354,7 @@ export async function loadPublicAgentAccess(
|
|
|
346
354
|
) {
|
|
347
355
|
apiToken = signScopedAgentAccessToken({
|
|
348
356
|
resourceKind: CLIP_AGENT_ACCESS_TOKEN_PREFIX,
|
|
349
|
-
resourceId:
|
|
357
|
+
resourceId: scopedRecordingId,
|
|
350
358
|
ttlSeconds: CLIPS_AGENT_ACCESS_TTL_SECONDS,
|
|
351
359
|
});
|
|
352
360
|
}
|
|
@@ -363,7 +371,7 @@ export async function loadPublicAgentAccess(
|
|
|
363
371
|
allowed = true;
|
|
364
372
|
apiToken = signScopedAgentAccessToken({
|
|
365
373
|
resourceKind: CLIP_AGENT_ACCESS_TOKEN_PREFIX,
|
|
366
|
-
resourceId:
|
|
374
|
+
resourceId: scopedRecordingId,
|
|
367
375
|
ttlSeconds: CLIPS_AGENT_ACCESS_TTL_SECONDS,
|
|
368
376
|
});
|
|
369
377
|
}
|
|
@@ -55,3 +55,28 @@ export function verifySharePassword(
|
|
|
55
55
|
const b = createHash("sha256").update(expected, "utf8").digest();
|
|
56
56
|
return timingSafeEqual(a, b);
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
export function getRecordingAccessTokenResourceId(
|
|
60
|
+
recordingId: string,
|
|
61
|
+
storedPassword: string | null | undefined,
|
|
62
|
+
sharePasswordVersion: string | null | undefined,
|
|
63
|
+
): string {
|
|
64
|
+
if (
|
|
65
|
+
sharePasswordVersion === "initial" ||
|
|
66
|
+
sharePasswordVersion?.startsWith("legacy:")
|
|
67
|
+
) {
|
|
68
|
+
// Keep pre-version grants and grants from older writers valid until a password mutation.
|
|
69
|
+
return recordingId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Encrypted passwords scope grants to the ciphertext; other rows use the dedicated version.
|
|
73
|
+
const versionSource =
|
|
74
|
+
storedPassword && isEncryptedSecretValue(storedPassword)
|
|
75
|
+
? storedPassword
|
|
76
|
+
: sharePasswordVersion;
|
|
77
|
+
if (!versionSource) {
|
|
78
|
+
throw new Error("Recording access scope requires a password version");
|
|
79
|
+
}
|
|
80
|
+
const version = createHash("sha256").update(versionSource).digest("hex");
|
|
81
|
+
return `${recordingId}:access:${version}`;
|
|
82
|
+
}
|
|
@@ -1117,6 +1117,42 @@ export const migrations = runMigrations(
|
|
|
1117
1117
|
name: "recording-failure-backfill-completion",
|
|
1118
1118
|
sql: `ALTER TABLE clips_backfill_leases ADD COLUMN IF NOT EXISTS completed_at TEXT`,
|
|
1119
1119
|
},
|
|
1120
|
+
{
|
|
1121
|
+
version: 77,
|
|
1122
|
+
name: "recording-share-password-version",
|
|
1123
|
+
// guard:allow-unscoped — initializes each row from its existing token scope.
|
|
1124
|
+
// Keep the DDL atomic; the migration runner splits SQL on semicolons.
|
|
1125
|
+
sql: `
|
|
1126
|
+
DO 'BEGIN
|
|
1127
|
+
EXECUTE ''ALTER TABLE recordings ADD COLUMN IF NOT EXISTS share_password_version TEXT'';
|
|
1128
|
+
EXECUTE ''UPDATE recordings
|
|
1129
|
+
SET share_password_version = ''''legacy:'''' || updated_at
|
|
1130
|
+
WHERE share_password_version IS NULL'';
|
|
1131
|
+
EXECUTE ''ALTER TABLE recordings ALTER COLUMN share_password_version SET DEFAULT ''''initial'''''';
|
|
1132
|
+
EXECUTE ''ALTER TABLE recordings ALTER COLUMN share_password_version SET NOT NULL'';
|
|
1133
|
+
EXECUTE format(
|
|
1134
|
+
''CREATE OR REPLACE FUNCTION public.clips_recordings_rotate_share_password_version()
|
|
1135
|
+
RETURNS trigger
|
|
1136
|
+
LANGUAGE plpgsql
|
|
1137
|
+
AS %L'',
|
|
1138
|
+
$function$BEGIN
|
|
1139
|
+
IF NEW.password IS DISTINCT FROM OLD.password THEN
|
|
1140
|
+
NEW.share_password_version := ''password-change:'' || COALESCE(NEW.share_password_version, OLD.share_password_version, ''initial'');
|
|
1141
|
+
END IF;
|
|
1142
|
+
RETURN NEW;
|
|
1143
|
+
END;$function$
|
|
1144
|
+
);
|
|
1145
|
+
BEGIN
|
|
1146
|
+
EXECUTE ''CREATE TRIGGER clips_recordings_rotate_share_password_version
|
|
1147
|
+
BEFORE UPDATE OF password ON public.recordings
|
|
1148
|
+
FOR EACH ROW
|
|
1149
|
+
EXECUTE FUNCTION public.clips_recordings_rotate_share_password_version()'';
|
|
1150
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
1151
|
+
NULL;
|
|
1152
|
+
END;
|
|
1153
|
+
END';
|
|
1154
|
+
`,
|
|
1155
|
+
},
|
|
1120
1156
|
],
|
|
1121
1157
|
{ table: "clips_migrations" },
|
|
1122
1158
|
);
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
queryString,
|
|
30
30
|
} from "../lib/public-agent-context.js";
|
|
31
31
|
import { isRecordingExpiredForViewer } from "../lib/recording-page-access.js";
|
|
32
|
+
import { getRecordingAccessTokenResourceId } from "../lib/share-password.js";
|
|
32
33
|
|
|
33
34
|
const ssrHandler = createH3SSRHandler(
|
|
34
35
|
() => import("virtual:react-router/server-build"),
|
|
@@ -86,6 +87,8 @@ async function buildClipAgentDiscovery(event: H3Event): Promise<{
|
|
|
86
87
|
id: schema.recordings.id,
|
|
87
88
|
title: schema.recordings.title,
|
|
88
89
|
status: schema.recordings.status,
|
|
90
|
+
updatedAt: schema.recordings.updatedAt,
|
|
91
|
+
sharePasswordVersion: schema.recordings.sharePasswordVersion,
|
|
89
92
|
visibility: schema.recordings.visibility,
|
|
90
93
|
password: schema.recordings.password,
|
|
91
94
|
expiresAt: schema.recordings.expiresAt,
|
|
@@ -113,7 +116,11 @@ async function buildClipAgentDiscovery(event: H3Event): Promise<{
|
|
|
113
116
|
const tokenGrantsAgentAccess = suppliedToken
|
|
114
117
|
? verifyScopedAgentAccessToken(suppliedToken, {
|
|
115
118
|
resourceKind: CLIP_AGENT_ACCESS_TOKEN_PREFIX,
|
|
116
|
-
resourceId:
|
|
119
|
+
resourceId: getRecordingAccessTokenResourceId(
|
|
120
|
+
recording.id,
|
|
121
|
+
recording.password,
|
|
122
|
+
recording.sharePasswordVersion,
|
|
123
|
+
),
|
|
117
124
|
}).ok
|
|
118
125
|
: false;
|
|
119
126
|
const anonymousAccess =
|
|
@@ -49,7 +49,10 @@ import {
|
|
|
49
49
|
type RecordingVisibility,
|
|
50
50
|
} from "../../lib/recordings.js";
|
|
51
51
|
import { isSeekableRepairPending } from "../../lib/seekable-media-state.js";
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
getRecordingAccessTokenResourceId,
|
|
54
|
+
verifySharePassword,
|
|
55
|
+
} from "../../lib/share-password.js";
|
|
53
56
|
import { hydrateCommentAuthorNames } from "../../lib/user-identities.js";
|
|
54
57
|
|
|
55
58
|
function appPath(path: string): string {
|
|
@@ -95,9 +98,15 @@ function isHttpsRequest(event: H3Event): boolean {
|
|
|
95
98
|
function setProtectedMediaAccessCookie(
|
|
96
99
|
event: H3Event,
|
|
97
100
|
recordingId: string,
|
|
101
|
+
password: string | null | undefined,
|
|
102
|
+
sharePasswordVersion: string | null | undefined,
|
|
98
103
|
): string {
|
|
99
104
|
const token = signShortLivedToken({
|
|
100
|
-
resourceId:
|
|
105
|
+
resourceId: getRecordingAccessTokenResourceId(
|
|
106
|
+
recordingId,
|
|
107
|
+
password,
|
|
108
|
+
sharePasswordVersion,
|
|
109
|
+
),
|
|
101
110
|
ttlSeconds: PROTECTED_MEDIA_ACCESS_TTL_SECONDS,
|
|
102
111
|
});
|
|
103
112
|
const secure = isHttpsRequest(event);
|
|
@@ -213,7 +222,11 @@ export default defineEventHandler(async (event) => {
|
|
|
213
222
|
const tokenAllowsAgentAccess = suppliedAgentAccessToken
|
|
214
223
|
? verifyScopedAgentAccessToken(suppliedAgentAccessToken, {
|
|
215
224
|
resourceKind: CLIP_AGENT_ACCESS_TOKEN_PREFIX,
|
|
216
|
-
resourceId:
|
|
225
|
+
resourceId: getRecordingAccessTokenResourceId(
|
|
226
|
+
rec.id,
|
|
227
|
+
rec.password,
|
|
228
|
+
rec.sharePasswordVersion,
|
|
229
|
+
),
|
|
217
230
|
}).ok
|
|
218
231
|
: false;
|
|
219
232
|
|
|
@@ -294,9 +307,19 @@ export default defineEventHandler(async (event) => {
|
|
|
294
307
|
return { error: "Password required", passwordRequired: true };
|
|
295
308
|
}
|
|
296
309
|
}
|
|
297
|
-
protectedMediaToken = setProtectedMediaAccessCookie(
|
|
310
|
+
protectedMediaToken = setProtectedMediaAccessCookie(
|
|
311
|
+
event,
|
|
312
|
+
recordingId,
|
|
313
|
+
rec.password,
|
|
314
|
+
rec.sharePasswordVersion,
|
|
315
|
+
);
|
|
298
316
|
} else if (tokenAllowsAgentAccess && !viewerIsOwner) {
|
|
299
|
-
protectedMediaToken = setProtectedMediaAccessCookie(
|
|
317
|
+
protectedMediaToken = setProtectedMediaAccessCookie(
|
|
318
|
+
event,
|
|
319
|
+
recordingId,
|
|
320
|
+
rec.password,
|
|
321
|
+
rec.sharePasswordVersion,
|
|
322
|
+
);
|
|
300
323
|
}
|
|
301
324
|
|
|
302
325
|
const [transcript] = await db
|
|
@@ -405,7 +428,11 @@ export default defineEventHandler(async (event) => {
|
|
|
405
428
|
: canExposeAgentContext && rec.password
|
|
406
429
|
? signScopedAgentAccessToken({
|
|
407
430
|
resourceKind: CLIP_AGENT_ACCESS_TOKEN_PREFIX,
|
|
408
|
-
resourceId:
|
|
431
|
+
resourceId: getRecordingAccessTokenResourceId(
|
|
432
|
+
recordingId,
|
|
433
|
+
rec.password,
|
|
434
|
+
rec.sharePasswordVersion,
|
|
435
|
+
),
|
|
409
436
|
})
|
|
410
437
|
: undefined;
|
|
411
438
|
const agentContextUrl = canExposeAgentContext
|
|
@@ -30,7 +30,10 @@ import {
|
|
|
30
30
|
} from "../../../lib/pending-redactions.js";
|
|
31
31
|
import { isRecordingExpiredForViewer } from "../../../lib/recording-page-access.js";
|
|
32
32
|
import { getOrganizationRoleForEmail } from "../../../lib/recordings.js";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
getRecordingAccessTokenResourceId,
|
|
35
|
+
verifySharePassword,
|
|
36
|
+
} from "../../../lib/share-password.js";
|
|
34
37
|
|
|
35
38
|
const FETCH_TIMEOUT_MS = 30_000;
|
|
36
39
|
const PROTECTED_MEDIA_ACCESS_TTL_SECONDS = 6 * 60 * 60;
|
|
@@ -53,6 +56,8 @@ type ThumbnailRecording = {
|
|
|
53
56
|
expiresAt?: string | null;
|
|
54
57
|
organizationId?: string | null;
|
|
55
58
|
password?: string | null;
|
|
59
|
+
sharePasswordVersion?: string | null;
|
|
60
|
+
updatedAt?: string | null;
|
|
56
61
|
visibility?: string | null;
|
|
57
62
|
};
|
|
58
63
|
|
|
@@ -79,9 +84,18 @@ function isHttpsRequest(event: H3Event): boolean {
|
|
|
79
84
|
);
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
function renewProtectedMediaCookie(
|
|
87
|
+
function renewProtectedMediaCookie(
|
|
88
|
+
event: H3Event,
|
|
89
|
+
recordingId: string,
|
|
90
|
+
password: string | null | undefined,
|
|
91
|
+
sharePasswordVersion: string | null | undefined,
|
|
92
|
+
): void {
|
|
83
93
|
const token = signShortLivedToken({
|
|
84
|
-
resourceId:
|
|
94
|
+
resourceId: getRecordingAccessTokenResourceId(
|
|
95
|
+
recordingId,
|
|
96
|
+
password,
|
|
97
|
+
sharePasswordVersion,
|
|
98
|
+
),
|
|
85
99
|
ttlSeconds: PROTECTED_MEDIA_ACCESS_TTL_SECONDS,
|
|
86
100
|
});
|
|
87
101
|
const secure = isHttpsRequest(event);
|
|
@@ -211,6 +225,8 @@ async function loadRecording(recordingId: string, event: H3Event) {
|
|
|
211
225
|
expiresAt: schema.recordings.expiresAt,
|
|
212
226
|
organizationId: schema.recordings.organizationId,
|
|
213
227
|
password: schema.recordings.password,
|
|
228
|
+
sharePasswordVersion: schema.recordings.sharePasswordVersion,
|
|
229
|
+
updatedAt: schema.recordings.updatedAt,
|
|
214
230
|
visibility: schema.recordings.visibility,
|
|
215
231
|
})
|
|
216
232
|
.from(schema.recordings)
|
|
@@ -280,17 +296,29 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
280
296
|
if (recording.password && loaded.role !== "owner") {
|
|
281
297
|
const queryToken = typeof query.t === "string" ? query.t : "";
|
|
282
298
|
const cookieToken = getCookie(event, cookieName(recordingId)) ?? "";
|
|
299
|
+
const scopedRecordingId = getRecordingAccessTokenResourceId(
|
|
300
|
+
recordingId,
|
|
301
|
+
recording.password,
|
|
302
|
+
recording.sharePasswordVersion,
|
|
303
|
+
);
|
|
283
304
|
const password =
|
|
284
305
|
typeof query.password === "string" ? query.password : "";
|
|
285
306
|
const allowed =
|
|
286
|
-
(queryToken &&
|
|
287
|
-
|
|
307
|
+
(queryToken &&
|
|
308
|
+
verifyShortLivedToken(queryToken, scopedRecordingId).ok) ||
|
|
309
|
+
(cookieToken &&
|
|
310
|
+
verifyShortLivedToken(cookieToken, scopedRecordingId).ok) ||
|
|
288
311
|
(password && verifySharePassword(password, recording.password));
|
|
289
312
|
if (!allowed) {
|
|
290
313
|
setResponseStatus(event, 401);
|
|
291
314
|
return { error: "Password required", passwordRequired: true };
|
|
292
315
|
}
|
|
293
|
-
renewProtectedMediaCookie(
|
|
316
|
+
renewProtectedMediaCookie(
|
|
317
|
+
event,
|
|
318
|
+
recordingId,
|
|
319
|
+
recording.password,
|
|
320
|
+
recording.sharePasswordVersion,
|
|
321
|
+
);
|
|
294
322
|
}
|
|
295
323
|
|
|
296
324
|
const sourceUrl =
|