@devultur/convex 1.4.3 → 1.5.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/README.md CHANGED
@@ -107,8 +107,13 @@ export const { createUploadUrl, confirmUpload, issueViewerToken, getMedia, listM
107
107
  signed-in user can't read or overwrite someone else's progress. Return `{ userId, canActForOthers: true }` for a
108
108
  caller who may (an admin resetting a learner's progress). A guard that returns no `userId` keeps the browser's, as
109
109
  `exposeApi` does: return one whenever the progress functions are exported.
110
+ - The guard may also return a `rewrite`, `{ name, args }`: each key of `args` replaces the caller's argument of that
111
+ name, on your server, before the function runs (see [Upload visibility](#upload-visibility)). `name` is the guarded
112
+ operation's, so `args` are typed for it; a rewrite named for another operation throws. The rewritten arguments
113
+ aren't validated again.
110
114
  - `visibility: "internal"` registers them as internal functions, for your own actions and mutations to call.
111
- - `config` takes what `Devultur.exposeApi` does (transcode preset, caption locales, upload lock).
115
+ - `config` takes what `Devultur.exposeApi` does (transcode preset, caption locales, upload lock and default
116
+ visibility).
112
117
  - Each function keeps its arguments and result types, so `api.devultur.confirmUpload` is typed as before and
113
118
  `<DevulturProvider api={api.devultur}>` takes them as it takes `exposeApi`'s.
114
119
 
@@ -117,6 +122,44 @@ export const { createUploadUrl, confirmUpload, issueViewerToken, getMedia, listM
117
122
  > use it only for a deployment where every caller is trusted, or move to `defineDevulturFunctions`, which takes the
118
123
  > same `config`.
119
124
 
125
+ ## Upload visibility
126
+
127
+ `config.upload.visibility` sets the visibility of an upload whose caller passes none. Without it, the Worker's default
128
+ by type applies.
129
+
130
+ ```typescript
131
+ export const { createUploadUrl, confirmUpload } = defineDevulturFunctions(devultur, {
132
+ guard,
133
+ config: { upload: { visibility: "public" } }, // a site whose uploads are public unless the browser asks otherwise
134
+ });
135
+ ```
136
+
137
+ The browser can still pass `visibility` (`upload(file, { visibility })`, `useMediaUpload({ visibility })`). To decide
138
+ it on your server instead, rewrite it in the guard: the guard's `rewrite` replaces the browser's arguments.
139
+
140
+ ```typescript
141
+ guard: async (ctx, operation) => {
142
+ const identity = await ctx.auth.getUserIdentity();
143
+ if (!identity) throw new ConvexError("Sign in first");
144
+ // Members' uploads are always private, whatever the browser asked for.
145
+ if (operation.name === "createUploadUrl") {
146
+ return { userId: identity.subject, rewrite: { name: "createUploadUrl", args: { visibility: "private" } } };
147
+ }
148
+ return { userId: identity.subject };
149
+ },
150
+ ```
151
+
152
+ Which one wins, from first to last:
153
+
154
+ 1. The guard's `rewrite.args.visibility`.
155
+ 2. The caller's `visibility`.
156
+ 3. `config.upload.visibility`.
157
+ 4. The Worker's default by type.
158
+
159
+ The guard sees the caller's arguments as they arrived (`operation.args.visibility` is `undefined` when the browser
160
+ passed none): the config's default is applied after the guard. `config.upload.lock` isn't an argument, so neither
161
+ the browser nor the guard's `rewrite` change it.
162
+
120
163
  ## Upload progress
121
164
 
122
165
  `upload(file, { onProgress, signal })` reports the PUT's progress and resolves to the stored reference:
@@ -175,7 +218,7 @@ ignored. Your action returns what the component's action returns: `{ token, expi
175
218
  ### Display-only apps
176
219
 
177
220
  An app that only shows media (no uploads, no deletes) uses `<DevulturViewerProvider>`, which needs only the base URL
178
- and a viewer token action:
221
+ and, for private media, a viewer token action:
179
222
 
180
223
  ```tsx
181
224
  import { DevulturViewerProvider, Preview } from "@devultur/convex/react";
@@ -194,13 +237,49 @@ import { DevulturViewerProvider, Preview } from "@devultur/convex/react";
194
237
  that need the `api` (`useDevulturApi`, `useMediaUpload`, `<MediaLibrary>`, `<MediaPicker>`), throw a
195
238
  `DevulturCapabilityError` whose `code` is `capability_unavailable`.
196
239
 
240
+ Like `<DevulturProvider>`, it issues the viewer token as soon as it mounts. With `lazyToken`, it issues it only when
241
+ a private media is shown, and renews it from then on: while every media on the page is public, it calls nothing,
242
+ `tokenStatus` stays `"idle"` and `token` stays `null`. Code that reads `token` itself then calls
243
+ `useDevultur().requestToken()` first; the built-in components and hooks do it for private media.
244
+
245
+ ```tsx
246
+ <DevulturViewerProvider baseUrl={MEDIA_URL} issueViewerToken={api.devultur.issueViewerToken} lazyToken>
247
+ <Gallery />
248
+ </DevulturViewerProvider>
249
+ ```
250
+
251
+ When all the media is public, leave `issueViewerToken` out. The provider then needs no Convex client and issues
252
+ nothing:
253
+
254
+ ```tsx
255
+ <DevulturViewerProvider baseUrl={MEDIA_URL}>
256
+ <Image media={post.cover} alt={post.title} />
257
+ </DevulturViewerProvider>
258
+ ```
259
+
260
+ A private media shown under it fails with `token_unavailable`, and `tokenError` says the provider has no
261
+ `issueViewerToken`.
262
+
263
+ ### Components outside a provider
264
+
265
+ `useDevultur()` throws outside `<DevulturProvider>` and `<DevulturViewerProvider>`. A component that also renders
266
+ without one uses `useDevulturOptional()`, which returns the same value, or `null` outside both:
267
+
268
+ ```tsx
269
+ function Avatar({ media, fallbackUrl }) {
270
+ const devultur = useDevulturOptional();
271
+ if (!devultur || !media) return <img src={fallbackUrl} alt="" />;
272
+ return <Image media={media} alt="" width={64} />;
273
+ }
274
+ ```
275
+
197
276
  ### When the viewer token can't be issued
198
277
 
199
278
  `useDevultur()` tells where the viewer token stands:
200
279
 
201
280
  ```tsx
202
281
  const { tokenStatus, tokenError, retryToken } = useDevultur();
203
- // tokenStatus: "idle" | "loading" | "ready" | "error"
282
+ // tokenStatus: "idle" (not asked for yet) | "loading" | "ready" | "error"
204
283
  if (tokenStatus === "error") return <button onClick={retryToken}>Try again</button>; // tokenError has why
205
284
  ```
206
285
 
@@ -210,6 +289,10 @@ there until `retryToken()`: a failed issuance isn't retried on its own. Meanwhil
210
289
  `<Preview>` passes it to its `fallback`, `<PdfThumbnail>` and `<Image>` render their `fallback` (`<Image onLoadError>`
211
290
  receives it), and `<DownloadLink onDownloadError>` receives it on click. They load once `retryToken()` gets a token.
212
291
 
292
+ `<Preview>`'s video plays in `<VideoPlayer>` from `@devultur/react`, with its playback metrics on: anonymous, no
293
+ cookies, sent to the media host (see [Playback metrics](https://github.com/devultur/devultur/blob/main/docs/guides/video-player.md#playback-metrics)).
294
+ `<Preview metrics={false}>` turns them off. A private video shows `loading` until its viewer token is issued.
295
+
213
296
  `<Preview>`'s video re-issues the token once when the delivery answers the playlist or a segment with a 401
214
297
  `session_mismatch`; after that, or on any other 401, it renders its `fallback` with `session_mismatch` or
215
298
  `token_unavailable`. `<VideoPlayer onError>` (from `@devultur/react`) reports these failures when you render the
@@ -658,8 +741,8 @@ the same name.
658
741
 
659
742
  `@devultur/convex/locales/es` addresses the reader with «tú» ("Vuelve a intentarlo"). For «usted» ("Vuelva a
660
743
  intentarlo", "No tiene permiso…"), import the same names from `@devultur/convex/locales/es-formal`: it exports exactly
661
- what `locales/es` does (error, attachment, preview, library and picker labels), and differs only in the strings that
662
- address the reader.
744
+ what `locales/es` does (error, attachment, preview, library, picker and video player labels), and differs only in the
745
+ strings that address the reader.
663
746
 
664
747
  ```tsx
665
748
  import { describeDevulturError } from "@devultur/convex";
@@ -685,6 +768,18 @@ Pass a partial map to change some messages and keep the rest, or a full one for
685
768
  describeDevulturError(error, { ...spanishLabels, lock_conflict: "Alguien más acaba de cambiarlo." });
686
769
  ```
687
770
 
771
+ ### Video player labels
772
+
773
+ `<VideoPlayer>` from `@devultur/react` takes `labels` like the components here do. Both Spanish locales export
774
+ `videoPlayerLabels`:
775
+
776
+ ```tsx
777
+ import { VideoPlayer } from "@devultur/react";
778
+ import { videoPlayerLabels } from "@devultur/convex/locales/es";
779
+
780
+ <VideoPlayer {...useVideoProps(lesson.video)} labels={videoPlayerLabels} />;
781
+ ```
782
+
688
783
  ## Object lock
689
784
 
690
785
  - `devultur.createUploadUrl(ctx, { filename, contentType, lock, hold })` locks a file from its first write.
@@ -175,11 +175,30 @@ var mediaPickerLabels = {
175
175
  removed: (name) => `${name} se quit\xF3.`,
176
176
  cleared: "Se limpi\xF3 la selecci\xF3n."
177
177
  };
178
+ var videoPlayerLabels = {
179
+ player: "Reproductor de video",
180
+ controls: "Controles del video",
181
+ play: "Reproducir",
182
+ pause: "Pausar",
183
+ seek: "Posici\xF3n del video",
184
+ mute: "Silenciar",
185
+ unmute: "Activar el sonido",
186
+ volume: "Volumen",
187
+ captions: "Subt\xEDtulos",
188
+ captionsOff: "Desactivados",
189
+ quality: "Calidad del video",
190
+ qualityAuto: "Autom\xE1tica",
191
+ speed: "Velocidad de reproducci\xF3n",
192
+ speedNormal: "Normal",
193
+ fullscreen: "Pantalla completa",
194
+ exitFullscreen: "Salir de pantalla completa"
195
+ };
178
196
 
179
197
  export {
180
198
  devulturErrorLabels,
181
199
  previewLabels,
182
200
  attachmentLabels,
183
201
  mediaLibraryLabels,
184
- mediaPickerLabels
202
+ mediaPickerLabels,
203
+ videoPlayerLabels
185
204
  };
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ import { ObjectLock, LockView, SetLockOptions, HoldPage, DevulturMedia } from '@
2
2
  export { CaptionLocale, DevulturConfig, DevulturKind, DevulturMedia, ImageFit, ImageFormat, SecurityLevel, SpeechProvider, StorageProvider, TranscodePreset, TranscodeProvider, TranslationProvider, VideoModuleConfig, Visibility } from '@devultur/core';
3
3
  import { FunctionReference, FunctionVisibility, DefaultFunctionArgs, GenericQueryCtx, GenericDataModel, GenericMutationCtx, GenericActionCtx, RegisteredQuery, RegisteredMutation, RegisteredAction, PaginationOptions, PaginationResult } from 'convex/server';
4
4
  import { PropertyValidators, Value, ConvexError } from 'convex/values';
5
- import { D as DevulturComponentConfig, M as MediaRecord } from './types-DZxN6Eij.js';
6
- export { P as ProgressRecord } from './types-DZxN6Eij.js';
5
+ import { D as DevulturComponentConfig, M as MediaRecord } from './types-C0ruHUcu.js';
6
+ export { a as DevulturUploadConfig, P as ProgressRecord } from './types-C0ruHUcu.js';
7
7
  export { a as DEVULTUR_ERROR_CODES, b as DevulturErrorCode, c as DevulturErrorDetails, d as DevulturErrorLabel, D as DevulturErrorLabels, e as describeDevulturError, f as devulturErrorLabels, i as isDevulturErrorCode } from './error-labels-D0Vbp-qA.js';
8
8
  export { dv } from './validators.js';
9
9
 
@@ -430,15 +430,32 @@ type DevulturGuardCtx<DataModel extends GenericDataModel = any> = GenericQueryCt
430
430
  /**
431
431
  * Throws to deny the call. May return what it learned about the caller: a `userId` becomes the subject of the token
432
432
  * `issueViewerToken` issues, and the only user the progress functions act for (another `userId` in their arguments is
433
- * refused with `user_mismatch`, unless the guard also returns `canActForOthers: true`).
433
+ * refused with `user_mismatch`, unless the guard also returns `canActForOthers: true`). May also return a `rewrite` of
434
+ * the caller's arguments (see `DevulturArgsRewrite`).
434
435
  */
435
- type DevulturGuard = (ctx: DevulturGuardCtx, operation: DevulturOperation) => GuardResult | Promise<GuardResult>;
436
- type GuardResult = CallerContext | undefined | void;
436
+ type DevulturGuard = (ctx: DevulturGuardCtx, operation: DevulturOperation) => DevulturGuardResult | Promise<DevulturGuardResult>;
437
+ /**
438
+ * Arguments a guard rewrites, on your server, before the function runs: each key of `args` replaces the caller's
439
+ * argument of that name. `name` is the operation's, so `args` are typed for it:
440
+ * `{ name: "createUploadUrl", args: { visibility: "private" } }` uploads privately whatever the browser asked for.
441
+ * A rewrite named for another operation than the one being guarded throws. The rewritten arguments aren't validated
442
+ * again.
443
+ */
444
+ type DevulturArgsRewrite = {
445
+ [Name in ExposedFunctionName]: {
446
+ name: Name;
447
+ args: Partial<ArgsOf<ExposedFunctions[Name]>>;
448
+ };
449
+ }[ExposedFunctionName];
450
+ /** What a guard returns: what it learned about the caller, and the arguments it rewrites. */
451
+ type DevulturGuardResult = (CallerContext & {
452
+ rewrite?: DevulturArgsRewrite;
453
+ }) | undefined | void;
437
454
  interface DevulturFunctionsOptions<Visibility extends FunctionVisibility> {
438
455
  guard: DevulturGuard;
439
456
  /** `"internal"` registers them as internal functions (callable from your server only). Default `"public"`. */
440
457
  visibility?: Visibility;
441
- /** Same as `Devultur.exposeApi`'s: the transcode preset, caption locales and upload lock. */
458
+ /** Same as `Devultur.exposeApi`'s: the transcode preset, caption locales, upload lock and default visibility. */
442
459
  config?: DevulturComponentConfig;
443
460
  }
444
461
  /** `Devultur.exposeApi`'s functions, each calling `options.guard` before it runs. Export the ones your app uses. */
@@ -451,4 +468,4 @@ declare function defineDevulturFunctions<Visibility extends FunctionVisibility =
451
468
  */
452
469
  declare function mediaFromRecord(record: MediaRecord): DevulturMedia | null;
453
470
 
454
- export { type CallerContext, Devultur, DevulturComponentConfig, type DevulturErrorData, type DevulturFunctionsOptions, type DevulturGuard, type DevulturGuardCtx, type DevulturOperation, MediaRecord, type RegisteredFunctions, defineDevulturFunctions, devulturErrorOf, isDevulturError, mediaFromRecord };
471
+ export { type CallerContext, Devultur, type DevulturArgsRewrite, DevulturComponentConfig, type DevulturErrorData, type DevulturFunctionsOptions, type DevulturGuard, type DevulturGuardCtx, type DevulturGuardResult, type DevulturOperation, MediaRecord, type RegisteredFunctions, defineDevulturFunctions, devulturErrorOf, isDevulturError, mediaFromRecord };
package/dist/index.js CHANGED
@@ -53,12 +53,16 @@ function progressArgsFor(args, caller) {
53
53
  });
54
54
  }
55
55
  function mediaIdOf(key) {
56
+ if (typeof key !== "string") {
57
+ throw new ConvexError({ code: "invalid_key", message: "The media key must be a string", status: 400 });
58
+ }
56
59
  return key.includes("/") ? extractId(key) : key;
57
60
  }
58
61
  function exposedFunctions(component, config) {
59
62
  const preset = config?.video?.transcodePreset;
60
63
  const locales = config?.video?.captionLocales ?? [];
61
64
  const uploadLock = config?.upload?.lock;
65
+ const defaultVisibility = config?.upload?.visibility;
62
66
  return {
63
67
  issueViewerToken: exposed(
64
68
  "action",
@@ -68,11 +72,18 @@ function exposedFunctions(component, config) {
68
72
  ...caller.userId ? { userId: caller.userId } : {}
69
73
  })
70
74
  ),
71
- // Optional per-resource visibility; omitted → the component resolves the config's default (secure-by-default).
75
+ // Visibility: the caller's (after the guard's rewrite), else `config.upload.visibility`, else the Worker's default.
72
76
  createUploadUrl: exposed(
73
77
  "action",
74
78
  { filename: v.string(), contentType: v.string(), visibility: visibilityArg },
75
- async (ctx, args) => ctx.runAction(component.public.createUploadUrl, { ...args, ...uploadLock ? { lock: uploadLock } : {} })
79
+ async (ctx, args) => {
80
+ const visibility = args.visibility ?? defaultVisibility;
81
+ return ctx.runAction(component.public.createUploadUrl, {
82
+ ...args,
83
+ ...visibility ? { visibility } : {},
84
+ ...uploadLock ? { lock: uploadLock } : {}
85
+ });
86
+ }
76
87
  ),
77
88
  // Records an upload once its bytes are stored (the Worker HEADs the key): a browser can't list anything
78
89
  // that isn't in your bucket. `useDevultur().upload()` and `useMediaUpload` call it after the PUT.
@@ -263,11 +274,18 @@ function defineDevulturFunctions(devultur, options) {
263
274
  return registerFunctions(exposedFunctions(devultur.component, config), visibility, (name, fn) => {
264
275
  return async (ctx, args) => {
265
276
  const operation = { name, kind: fn.kind, args };
266
- const caller = await guard(ctx, operation) ?? {};
267
- return fn.handler(ctx, args, caller);
277
+ const { rewrite, ...caller } = await guard(ctx, operation) ?? {};
278
+ return fn.handler(ctx, rewrittenArgs(name, args, rewrite), caller);
268
279
  };
269
280
  });
270
281
  }
282
+ function rewrittenArgs(name, args, rewrite) {
283
+ if (!rewrite) return args;
284
+ if (rewrite.name !== name) {
285
+ throw new Error(`The guard rewrote ${rewrite.name}'s arguments while guarding ${name}`);
286
+ }
287
+ return { ...args, ...rewrite.args };
288
+ }
271
289
  export {
272
290
  CaptionLocale,
273
291
  DEVULTUR_ERROR_CODES,
@@ -1,6 +1,7 @@
1
- import { A as AttachmentLabels, M as MediaLibraryLabels, a as MediaPickerLabels, P as PreviewLabels } from '../preview-labels-B7HNa7Va.js';
1
+ import { VideoPlayerLabels } from '@devultur/react';
2
+ import { A as AttachmentLabels, M as MediaLibraryLabels, a as MediaPickerLabels, P as PreviewLabels } from '../preview-labels-C2nUUY_f.js';
2
3
  import { D as DevulturErrorLabels } from '../error-labels-D0Vbp-qA.js';
3
- import '../types-DZxN6Eij.js';
4
+ import '../types-C0ruHUcu.js';
4
5
  import '@devultur/core';
5
6
  import 'convex/values';
6
7
  import 'convex/server';
@@ -10,5 +11,6 @@ declare const attachmentLabels: AttachmentLabels;
10
11
  declare const previewLabels: PreviewLabels;
11
12
  declare const mediaLibraryLabels: MediaLibraryLabels;
12
13
  declare const mediaPickerLabels: MediaPickerLabels;
14
+ declare const videoPlayerLabels: VideoPlayerLabels;
13
15
 
14
- export { attachmentLabels, devulturErrorLabels, mediaLibraryLabels, mediaPickerLabels, previewLabels };
16
+ export { attachmentLabels, devulturErrorLabels, mediaLibraryLabels, mediaPickerLabels, previewLabels, videoPlayerLabels };
@@ -3,8 +3,9 @@ import {
3
3
  devulturErrorLabels,
4
4
  mediaLibraryLabels,
5
5
  mediaPickerLabels,
6
- previewLabels
7
- } from "../chunk-JZYKPLI4.js";
6
+ previewLabels,
7
+ videoPlayerLabels
8
+ } from "../chunk-RDVBE3G3.js";
8
9
  import "../chunk-QBHIZBOQ.js";
9
10
  import {
10
11
  describeDevulturError,
@@ -55,10 +56,12 @@ var mediaPickerLabels2 = {
55
56
  dropHint: (multiple) => multiple ? "o suelte los archivos aqu\xED" : "o suelte un archivo aqu\xED",
56
57
  uploadError: (error) => describeDevulturError(error, devulturErrorLabels2) ?? `La subida fall\xF3. ${TRY_AGAIN}`
57
58
  };
59
+ var videoPlayerLabels2 = videoPlayerLabels;
58
60
  export {
59
61
  attachmentLabels2 as attachmentLabels,
60
62
  devulturErrorLabels2 as devulturErrorLabels,
61
63
  mediaLibraryLabels2 as mediaLibraryLabels,
62
64
  mediaPickerLabels2 as mediaPickerLabels,
63
- previewLabels2 as previewLabels
65
+ previewLabels2 as previewLabels,
66
+ videoPlayerLabels2 as videoPlayerLabels
64
67
  };
@@ -1,6 +1,7 @@
1
- import { A as AttachmentLabels, M as MediaLibraryLabels, a as MediaPickerLabels, P as PreviewLabels } from '../preview-labels-B7HNa7Va.js';
1
+ import { VideoPlayerLabels } from '@devultur/react';
2
+ import { A as AttachmentLabels, M as MediaLibraryLabels, a as MediaPickerLabels, P as PreviewLabels } from '../preview-labels-C2nUUY_f.js';
2
3
  import { D as DevulturErrorLabels } from '../error-labels-D0Vbp-qA.js';
3
- import '../types-DZxN6Eij.js';
4
+ import '../types-C0ruHUcu.js';
4
5
  import '@devultur/core';
5
6
  import 'convex/values';
6
7
  import 'convex/server';
@@ -10,5 +11,6 @@ declare const previewLabels: PreviewLabels;
10
11
  declare const attachmentLabels: AttachmentLabels;
11
12
  declare const mediaLibraryLabels: MediaLibraryLabels;
12
13
  declare const mediaPickerLabels: MediaPickerLabels;
14
+ declare const videoPlayerLabels: VideoPlayerLabels;
13
15
 
14
- export { attachmentLabels, devulturErrorLabels, mediaLibraryLabels, mediaPickerLabels, previewLabels };
16
+ export { attachmentLabels, devulturErrorLabels, mediaLibraryLabels, mediaPickerLabels, previewLabels, videoPlayerLabels };
@@ -3,8 +3,9 @@ import {
3
3
  devulturErrorLabels,
4
4
  mediaLibraryLabels,
5
5
  mediaPickerLabels,
6
- previewLabels
7
- } from "../chunk-JZYKPLI4.js";
6
+ previewLabels,
7
+ videoPlayerLabels
8
+ } from "../chunk-RDVBE3G3.js";
8
9
  import "../chunk-QBHIZBOQ.js";
9
10
  import "../chunk-YYOZ3PDH.js";
10
11
  export {
@@ -12,5 +13,6 @@ export {
12
13
  devulturErrorLabels,
13
14
  mediaLibraryLabels,
14
15
  mediaPickerLabels,
15
- previewLabels
16
+ previewLabels,
17
+ videoPlayerLabels
16
18
  };
@@ -1,4 +1,4 @@
1
- import { M as MediaRecord } from './types-DZxN6Eij.js';
1
+ import { M as MediaRecord } from './types-C0ruHUcu.js';
2
2
 
3
3
  /** What an attachment is, for its icon and type label. */
4
4
  type AttachmentKind = "image" | "video" | "pdf" | "audio" | "text" | "file";