@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.
Files changed (86) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +17 -0
  3. package/corpus/templates/assets/CHANGELOG.md +18 -0
  4. package/corpus/templates/brain/CHANGELOG.md +6 -0
  5. package/corpus/templates/calendar/CHANGELOG.md +4 -0
  6. package/corpus/templates/clips/CHANGELOG.md +19 -0
  7. package/corpus/templates/clips/actions/create-recording-agent-link.ts +8 -2
  8. package/corpus/templates/clips/actions/update-recording.ts +5 -1
  9. package/corpus/templates/clips/app/routes/share.$shareId.tsx +10 -3
  10. package/corpus/templates/clips/changelog/2026-09-28-agent-links-stay-valid-when-clip-details-change.md +6 -0
  11. package/corpus/templates/clips/changelog/2026-09-28-password-changes-immediately-invalidate-old-recording-links.md +6 -0
  12. package/corpus/templates/clips/changelog/2026-09-28-password-protected-recording-links-stop-working-when-the-pas.md +6 -0
  13. package/corpus/templates/clips/server/db/schema.ts +3 -0
  14. package/corpus/templates/clips/server/lib/player-video-url.ts +9 -1
  15. package/corpus/templates/clips/server/lib/public-agent-context.ts +12 -4
  16. package/corpus/templates/clips/server/lib/share-password.ts +25 -0
  17. package/corpus/templates/clips/server/plugins/db.ts +36 -0
  18. package/corpus/templates/clips/server/routes/[...page].get.ts +8 -1
  19. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +33 -6
  20. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +34 -6
  21. package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +26 -5
  22. package/corpus/templates/content/CHANGELOG.md +1 -0
  23. package/corpus/templates/crm/CHANGELOG.md +12 -0
  24. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +265 -3
  25. package/corpus/templates/design/CHANGELOG.md +21 -0
  26. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +463 -6
  27. package/corpus/templates/design/app/pages/Index.tsx +1 -1
  28. package/corpus/templates/design/changelog/2026-09-27-auto-focused-app-fields-no-longer-interrupt-canvas-shortcuts.md +6 -0
  29. package/corpus/templates/design/changelog/2026-09-28-model-picker-loads-available-models.md +6 -0
  30. package/corpus/templates/design/changelog/2026-09-28-template-menus-stay-available-on-hover.md +6 -0
  31. package/corpus/templates/dispatch/CHANGELOG.md +18 -0
  32. package/corpus/templates/forms/CHANGELOG.md +6 -0
  33. package/corpus/templates/mail/.env.example +1 -1
  34. package/corpus/templates/mail/CHANGELOG.md +33 -0
  35. package/corpus/templates/mail/changelog/2026-09-28-fixed-inbox-loading-stalls-and-showed-progress-during-google-2.md +6 -0
  36. package/corpus/templates/mail/changelog/2026-09-28-fixed-inbox-loading-stalls-and-showed-progress-during-google.md +6 -0
  37. package/corpus/templates/mail/server/db/schema.ts +7 -0
  38. package/corpus/templates/mail/server/lib/ai-filter-backfill.ts +4 -0
  39. package/corpus/templates/mail/server/lib/jobs.ts +5 -2
  40. package/corpus/templates/mail/server/plugins/db.ts +16 -0
  41. package/corpus/templates/mail/server/plugins/mail-jobs.ts +355 -44
  42. package/corpus/templates/slides/CHANGELOG.md +28 -0
  43. package/corpus/templates/slides/app/pages/Index.tsx +1 -1
  44. package/corpus/templates/slides/changelog/2026-09-28-model-picker-loads-available-models.md +6 -0
  45. package/corpus/templates/slides/changelog/2026-09-28-template-menus-stay-available-on-hover.md +6 -0
  46. package/dist/automation/index.d.ts +1 -1
  47. package/dist/client/auth/AuthPage.js +3 -2
  48. package/dist/client/org/OrgSwitcher.js +31 -19
  49. package/dist/client/settings/UsageSection.js +3 -3
  50. package/dist/client/use-action.js +1 -1
  51. package/dist/collab/awareness.d.ts +2 -2
  52. package/dist/collab/routes.d.ts +1 -1
  53. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  54. package/dist/jobs/background-automation-runner.d.ts +2 -0
  55. package/dist/jobs/background-automation-runner.js +20 -1
  56. package/dist/jobs/sweep-hooks.d.ts +6 -2
  57. package/dist/jobs/sweep-hooks.js +17 -9
  58. package/dist/localization/core-messages/ar-SA.js +2 -2
  59. package/dist/localization/core-messages/de-DE.js +2 -2
  60. package/dist/localization/core-messages/en-US.d.ts +2 -2
  61. package/dist/localization/core-messages/en-US.js +2 -2
  62. package/dist/localization/core-messages/es-ES.js +2 -2
  63. package/dist/localization/core-messages/fr-FR.js +2 -2
  64. package/dist/localization/core-messages/hi-IN.js +2 -2
  65. package/dist/localization/core-messages/ja-JP.js +2 -2
  66. package/dist/localization/core-messages/ko-KR.js +2 -2
  67. package/dist/localization/core-messages/pt-BR.js +2 -2
  68. package/dist/localization/core-messages/zh-CN.js +2 -2
  69. package/dist/localization/core-messages/zh-TW.js +2 -2
  70. package/dist/localization/core-messages.d.ts +2 -2
  71. package/dist/notifications/routes.d.ts +5 -5
  72. package/dist/observability/routes.d.ts +6 -6
  73. package/dist/progress/routes.d.ts +1 -1
  74. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  75. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  76. package/dist/resources/handlers.d.ts +1 -1
  77. package/dist/secrets/routes.d.ts +4 -4
  78. package/dist/server/agent-chat-plugin.js +6 -2
  79. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  80. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  81. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  82. package/dist/server/index.d.ts +1 -1
  83. package/dist/triggers/dispatcher.js +167 -36
  84. package/dist/triggers/event-queue.d.ts +18 -1
  85. package/dist/triggers/event-queue.js +143 -10
  86. package/package.json +4 -4
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 10498
34
+ - template files: 10508
@@ -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?: string | null;
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: recording.id,
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: id,
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
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-09-28
4
+ ---
5
+
6
+ Agent links stay valid when clip details change
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: security
3
+ date: 2026-09-28
4
+ ---
5
+
6
+ Password changes immediately invalidate old recording links
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: security
3
+ date: 2026-09-28
4
+ ---
5
+
6
+ Password-protected recording links stop working when the password changes
@@ -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({ resourceId: recording.id });
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 { verifySharePassword } from "./share-password.js";
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: recording.id,
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: recording.id,
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: recording.id,
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: recording.id,
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 { verifySharePassword } from "../../lib/share-password.js";
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: recordingId,
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: rec.id,
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(event, recordingId);
310
+ protectedMediaToken = setProtectedMediaAccessCookie(
311
+ event,
312
+ recordingId,
313
+ rec.password,
314
+ rec.sharePasswordVersion,
315
+ );
298
316
  } else if (tokenAllowsAgentAccess && !viewerIsOwner) {
299
- protectedMediaToken = setProtectedMediaAccessCookie(event, recordingId);
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: recordingId,
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 { verifySharePassword } from "../../../lib/share-password.js";
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(event: H3Event, recordingId: string): void {
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: recordingId,
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 && verifyShortLivedToken(queryToken, recordingId).ok) ||
287
- (cookieToken && verifyShortLivedToken(cookieToken, recordingId).ok) ||
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(event, recordingId);
316
+ renewProtectedMediaCookie(
317
+ event,
318
+ recordingId,
319
+ recording.password,
320
+ recording.sharePasswordVersion,
321
+ );
294
322
  }
295
323
 
296
324
  const sourceUrl =