@devultur/convex 1.4.3 → 1.5.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/README.md +123 -7
- package/dist/chunk-BDEO67OT.js +110 -0
- package/dist/{chunk-JZYKPLI4.js → chunk-RDVBE3G3.js} +20 -1
- package/dist/component/http.js +2 -2
- package/dist/component/library.d.ts +2 -2
- package/dist/component/library.js +3 -3
- package/dist/component/lock.js +3 -3
- package/dist/component/mediaClient.js +1 -1
- package/dist/component/metadata.js +2 -2
- package/dist/component/public.js +9 -9
- package/dist/component/uploads.js +6 -6
- package/dist/component/workerErrors.d.ts +1 -1
- package/dist/component/workerErrors.js +1 -1
- package/dist/hooks-uqpBd5UF.d.ts +171 -0
- package/dist/index.d.ts +74 -20
- package/dist/index.js +23 -4
- package/dist/locales/es-formal.d.ts +5 -3
- package/dist/locales/es-formal.js +6 -3
- package/dist/locales/es.d.ts +5 -3
- package/dist/locales/es.js +5 -3
- package/dist/{preview-labels-B7HNa7Va.d.ts → preview-labels-C2nUUY_f.d.ts} +1 -1
- package/dist/react/hooks.d.ts +6 -0
- package/dist/react/hooks.js +23 -0
- package/dist/react.d.ts +157 -275
- package/dist/react.js +958 -931
- package/dist/{types-DZxN6Eij.d.ts → types-C0ruHUcu.d.ts} +14 -8
- package/docs/guides/object-lock.md +517 -0
- package/docs/guides/plugins.md +205 -0
- package/docs/guides/self-host.md +523 -0
- package/docs/guides/video-player.md +105 -0
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -107,16 +107,62 @@ 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
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
- `config` takes what `Devultur.exposeApi` does (transcode preset, caption locales, upload lock and default
|
|
116
|
+
visibility).
|
|
117
|
+
- Each function keeps its arguments and returns the component's result, typed: with no annotation,
|
|
118
|
+
`useQuery(api.devultur.getMedia, { key })` is a `MediaDocument | null` (a `MediaRecord` with its `_id` and
|
|
119
|
+
`_creationTime`), and `listMediaPage` and `searchMedia` are a `PaginationResult<MediaDocument>`.
|
|
120
|
+
`DevulturFunctionResults` names every function's result. `<DevulturProvider api={api.devultur}>` takes them as it
|
|
121
|
+
takes `exposeApi`'s, whose results are the same.
|
|
114
122
|
|
|
115
123
|
> **`Devultur.exposeApi` has no auth.** It returns the same functions with no guard: anyone who can reach your Convex
|
|
116
124
|
> deployment can create upload URLs, confirm uploads, list the whole library and delete media. It keeps working, but
|
|
117
125
|
> use it only for a deployment where every caller is trusted, or move to `defineDevulturFunctions`, which takes the
|
|
118
126
|
> same `config`.
|
|
119
127
|
|
|
128
|
+
## Upload visibility
|
|
129
|
+
|
|
130
|
+
`config.upload.visibility` sets the visibility of an upload whose caller passes none. Without it, the Worker's default
|
|
131
|
+
by type applies.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
export const { createUploadUrl, confirmUpload } = defineDevulturFunctions(devultur, {
|
|
135
|
+
guard,
|
|
136
|
+
config: { upload: { visibility: "public" } }, // a site whose uploads are public unless the browser asks otherwise
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The browser can still pass `visibility` (`upload(file, { visibility })`, `useMediaUpload({ visibility })`). To decide
|
|
141
|
+
it on your server instead, rewrite it in the guard: the guard's `rewrite` replaces the browser's arguments.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
guard: async (ctx, operation) => {
|
|
145
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
146
|
+
if (!identity) throw new ConvexError("Sign in first");
|
|
147
|
+
// Members' uploads are always private, whatever the browser asked for.
|
|
148
|
+
if (operation.name === "createUploadUrl") {
|
|
149
|
+
return { userId: identity.subject, rewrite: { name: "createUploadUrl", args: { visibility: "private" } } };
|
|
150
|
+
}
|
|
151
|
+
return { userId: identity.subject };
|
|
152
|
+
},
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Which one wins, from first to last:
|
|
156
|
+
|
|
157
|
+
1. The guard's `rewrite.args.visibility`.
|
|
158
|
+
2. The caller's `visibility`.
|
|
159
|
+
3. `config.upload.visibility`.
|
|
160
|
+
4. The Worker's default by type.
|
|
161
|
+
|
|
162
|
+
The guard sees the caller's arguments as they arrived (`operation.args.visibility` is `undefined` when the browser
|
|
163
|
+
passed none): the config's default is applied after the guard. `config.upload.lock` isn't an argument, so neither
|
|
164
|
+
the browser nor the guard's `rewrite` change it.
|
|
165
|
+
|
|
120
166
|
## Upload progress
|
|
121
167
|
|
|
122
168
|
`upload(file, { onProgress, signal })` reports the PUT's progress and resolves to the stored reference:
|
|
@@ -175,7 +221,7 @@ ignored. Your action returns what the component's action returns: `{ token, expi
|
|
|
175
221
|
### Display-only apps
|
|
176
222
|
|
|
177
223
|
An app that only shows media (no uploads, no deletes) uses `<DevulturViewerProvider>`, which needs only the base URL
|
|
178
|
-
and a viewer token action:
|
|
224
|
+
and, for private media, a viewer token action:
|
|
179
225
|
|
|
180
226
|
```tsx
|
|
181
227
|
import { DevulturViewerProvider, Preview } from "@devultur/convex/react";
|
|
@@ -194,13 +240,64 @@ import { DevulturViewerProvider, Preview } from "@devultur/convex/react";
|
|
|
194
240
|
that need the `api` (`useDevulturApi`, `useMediaUpload`, `<MediaLibrary>`, `<MediaPicker>`), throw a
|
|
195
241
|
`DevulturCapabilityError` whose `code` is `capability_unavailable`.
|
|
196
242
|
|
|
243
|
+
Like `<DevulturProvider>`, it issues the viewer token as soon as it mounts. With `lazyToken`, it issues it only when
|
|
244
|
+
a private media is shown, and renews it from then on: while every media on the page is public, it calls nothing,
|
|
245
|
+
`tokenStatus` stays `"idle"` and `token` stays `null`. Code that reads `token` itself then calls
|
|
246
|
+
`useDevultur().requestToken()` first; the built-in components and hooks do it for private media.
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
<DevulturViewerProvider baseUrl={MEDIA_URL} issueViewerToken={api.devultur.issueViewerToken} lazyToken>
|
|
250
|
+
<Gallery />
|
|
251
|
+
</DevulturViewerProvider>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
When all the media is public, leave `issueViewerToken` out. The provider then needs no Convex client and issues
|
|
255
|
+
nothing:
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
<DevulturViewerProvider baseUrl={MEDIA_URL}>
|
|
259
|
+
<Image media={post.cover} alt={post.title} />
|
|
260
|
+
</DevulturViewerProvider>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
A private media shown under it fails with `token_unavailable`, and `tokenError` says the provider has no
|
|
264
|
+
`issueViewerToken`.
|
|
265
|
+
|
|
266
|
+
### Components outside a provider
|
|
267
|
+
|
|
268
|
+
`useDevultur()` throws outside `<DevulturProvider>` and `<DevulturViewerProvider>`. A component that also renders
|
|
269
|
+
without one uses `useDevulturOptional()`, which returns the same value, or `null` outside both:
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
function Avatar({ media, fallbackUrl }) {
|
|
273
|
+
const devultur = useDevulturOptional();
|
|
274
|
+
if (!devultur || !media) return <img src={fallbackUrl} alt="" />;
|
|
275
|
+
return <Image media={media} alt="" width={64} />;
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Only the hooks
|
|
280
|
+
|
|
281
|
+
`@devultur/convex/react/hooks` exports the contexts and the hooks that only read them, without the components or
|
|
282
|
+
Convex's client: `useDevultur`, `useDevulturOptional`, `useDevulturApi`, `useDevulturProgress`, `useMediaUrl`,
|
|
283
|
+
`useVideoProps`, and the `DevulturContext`, `DevulturApiContext` and `DevulturProgressContext` objects. A shared
|
|
284
|
+
component that only checks for a provider adds well under 1 kB gzipped to the bundle:
|
|
285
|
+
|
|
286
|
+
```tsx
|
|
287
|
+
import { useDevulturOptional } from "@devultur/convex/react/hooks";
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
They're the very objects `@devultur/convex/react` exports, so a `<DevulturProvider>` or `<DevulturViewerProvider>`
|
|
291
|
+
imported from there fills them. The hooks that call your Convex functions (`useDevulturMedia`, `useVideoState`,
|
|
292
|
+
`useMediaUpload`, `useMediaLibrary`) stay in `@devultur/convex/react`, with the components.
|
|
293
|
+
|
|
197
294
|
### When the viewer token can't be issued
|
|
198
295
|
|
|
199
296
|
`useDevultur()` tells where the viewer token stands:
|
|
200
297
|
|
|
201
298
|
```tsx
|
|
202
299
|
const { tokenStatus, tokenError, retryToken } = useDevultur();
|
|
203
|
-
// tokenStatus: "idle" | "loading" | "ready" | "error"
|
|
300
|
+
// tokenStatus: "idle" (not asked for yet) | "loading" | "ready" | "error"
|
|
204
301
|
if (tokenStatus === "error") return <button onClick={retryToken}>Try again</button>; // tokenError has why
|
|
205
302
|
```
|
|
206
303
|
|
|
@@ -210,6 +307,11 @@ there until `retryToken()`: a failed issuance isn't retried on its own. Meanwhil
|
|
|
210
307
|
`<Preview>` passes it to its `fallback`, `<PdfThumbnail>` and `<Image>` render their `fallback` (`<Image onLoadError>`
|
|
211
308
|
receives it), and `<DownloadLink onDownloadError>` receives it on click. They load once `retryToken()` gets a token.
|
|
212
309
|
|
|
310
|
+
`<Preview>`'s video plays in `<VideoPlayer>` from `@devultur/react`, with its playback metrics on: anonymous, no
|
|
311
|
+
cookies, sent to the media host (see [Playback metrics](https://github.com/devultur/devultur/blob/main/docs/guides/video-player.md#playback-metrics),
|
|
312
|
+
also in `node_modules/@devultur/convex/docs/guides/video-player.md`).
|
|
313
|
+
`<Preview metrics={false}>` turns them off. A private video shows `loading` until its viewer token is issued.
|
|
314
|
+
|
|
213
315
|
`<Preview>`'s video re-issues the token once when the delivery answers the playlist or a segment with a 401
|
|
214
316
|
`session_mismatch`; after that, or on any other 401, it renders its `fallback` with `session_mismatch` or
|
|
215
317
|
`token_unavailable`. `<VideoPlayer onError>` (from `@devultur/react`) reports these failures when you render the
|
|
@@ -658,8 +760,8 @@ the same name.
|
|
|
658
760
|
|
|
659
761
|
`@devultur/convex/locales/es` addresses the reader with «tú» ("Vuelve a intentarlo"). For «usted» ("Vuelva a
|
|
660
762
|
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
|
|
662
|
-
address the reader.
|
|
763
|
+
what `locales/es` does (error, attachment, preview, library, picker and video player labels), and differs only in the
|
|
764
|
+
strings that address the reader.
|
|
663
765
|
|
|
664
766
|
```tsx
|
|
665
767
|
import { describeDevulturError } from "@devultur/convex";
|
|
@@ -685,6 +787,18 @@ Pass a partial map to change some messages and keep the rest, or a full one for
|
|
|
685
787
|
describeDevulturError(error, { ...spanishLabels, lock_conflict: "Alguien más acaba de cambiarlo." });
|
|
686
788
|
```
|
|
687
789
|
|
|
790
|
+
### Video player labels
|
|
791
|
+
|
|
792
|
+
`<VideoPlayer>` from `@devultur/react` takes `labels` like the components here do. Both Spanish locales export
|
|
793
|
+
`videoPlayerLabels`:
|
|
794
|
+
|
|
795
|
+
```tsx
|
|
796
|
+
import { VideoPlayer } from "@devultur/react";
|
|
797
|
+
import { videoPlayerLabels } from "@devultur/convex/locales/es";
|
|
798
|
+
|
|
799
|
+
<VideoPlayer {...useVideoProps(lesson.video)} labels={videoPlayerLabels} />;
|
|
800
|
+
```
|
|
801
|
+
|
|
688
802
|
## Object lock
|
|
689
803
|
|
|
690
804
|
- `devultur.createUploadUrl(ctx, { filename, contentType, lock, hold })` locks a file from its first write.
|
|
@@ -701,6 +815,8 @@ what's enforced and the caveats.
|
|
|
701
815
|
## Documentation
|
|
702
816
|
|
|
703
817
|
Guides and API reference: [github.com/devultur/devultur/tree/main/docs](https://github.com/devultur/devultur/tree/main/docs).
|
|
818
|
+
The guides this README links to ship with the package, for the installed version, in
|
|
819
|
+
`node_modules/@devultur/convex/docs/guides/`: `video-player.md`, `self-host.md`, `plugins.md` and `object-lock.md`.
|
|
704
820
|
|
|
705
821
|
## License
|
|
706
822
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/capability-error.ts
|
|
2
|
+
var DevulturCapabilityError = class extends Error {
|
|
3
|
+
code = "capability_unavailable";
|
|
4
|
+
capability;
|
|
5
|
+
constructor(capability) {
|
|
6
|
+
super(
|
|
7
|
+
`Devultur: \`${capability}\` isn't available under <DevulturViewerProvider>. Use <DevulturProvider api> with the actions it needs.`
|
|
8
|
+
);
|
|
9
|
+
this.name = "DevulturCapabilityError";
|
|
10
|
+
this.capability = capability;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// src/devultur-context.ts
|
|
15
|
+
import React from "react";
|
|
16
|
+
var DevulturContext = /* @__PURE__ */ React.createContext(null);
|
|
17
|
+
var DevulturApiContext = /* @__PURE__ */ React.createContext(null);
|
|
18
|
+
function useDevultur() {
|
|
19
|
+
const context = React.use(DevulturContext);
|
|
20
|
+
if (!context) throw new Error("useDevultur must be used within <DevulturProvider> or <DevulturViewerProvider>");
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
23
|
+
function useDevulturOptional() {
|
|
24
|
+
return React.use(DevulturContext);
|
|
25
|
+
}
|
|
26
|
+
function useDevulturApi() {
|
|
27
|
+
const api = React.use(DevulturApiContext);
|
|
28
|
+
if (api) return api;
|
|
29
|
+
if (React.use(DevulturContext)) throw new DevulturCapabilityError("api");
|
|
30
|
+
throw new Error("useDevulturApi must be used within <DevulturProvider>");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/progress-context.ts
|
|
34
|
+
import React2 from "react";
|
|
35
|
+
var DevulturProgressContext = /* @__PURE__ */ React2.createContext(null);
|
|
36
|
+
function useDevulturProgress() {
|
|
37
|
+
const context = React2.use(DevulturProgressContext);
|
|
38
|
+
if (!context) throw new Error("useDevulturProgress requires progress API in DevulturProvider");
|
|
39
|
+
return context;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/media-url-hooks.ts
|
|
43
|
+
import React4 from "react";
|
|
44
|
+
import {
|
|
45
|
+
buildMediaUrl,
|
|
46
|
+
CaptionLocaleLabel,
|
|
47
|
+
mediaCaptionPath,
|
|
48
|
+
mediaPath,
|
|
49
|
+
visibilityFromKey
|
|
50
|
+
} from "@devultur/core";
|
|
51
|
+
|
|
52
|
+
// src/request-token.ts
|
|
53
|
+
import React3 from "react";
|
|
54
|
+
function useRequestTokenWhen(session, isNeeded) {
|
|
55
|
+
const { requestToken, token } = session;
|
|
56
|
+
const hasToken = token !== null;
|
|
57
|
+
React3.useEffect(() => {
|
|
58
|
+
if (isNeeded && !hasToken) requestToken();
|
|
59
|
+
}, [isNeeded, hasToken, requestToken]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/media-url-hooks.ts
|
|
63
|
+
function useMediaUrl(key, options) {
|
|
64
|
+
const devultur = useDevultur();
|
|
65
|
+
const { token, baseUrl } = devultur;
|
|
66
|
+
useRequestTokenWhen(devultur, Boolean(key) && visibilityFromKey(key ?? "") !== "public");
|
|
67
|
+
return React4.useMemo(() => {
|
|
68
|
+
if (!key) return null;
|
|
69
|
+
if (!baseUrl) throw new Error("useMediaUrl requires `baseUrl` on <DevulturProvider>");
|
|
70
|
+
const isPublic = visibilityFromKey(key) === "public";
|
|
71
|
+
return buildMediaUrl(baseUrl, key, { ...options, token: isPublic ? void 0 : token ?? void 0 });
|
|
72
|
+
}, [key, baseUrl, token, options]);
|
|
73
|
+
}
|
|
74
|
+
function useVideoProps(media, options) {
|
|
75
|
+
const devultur = useDevultur();
|
|
76
|
+
const { token, baseUrl, sessionSecret } = devultur;
|
|
77
|
+
useRequestTokenWhen(devultur, media?.kind === "video" && media.visibility === "private");
|
|
78
|
+
const locales = options?.locales;
|
|
79
|
+
return React4.useMemo(() => {
|
|
80
|
+
if (media?.kind !== "video") return null;
|
|
81
|
+
if (!baseUrl) throw new Error("useVideoProps requires `baseUrl` on <DevulturProvider>");
|
|
82
|
+
const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
|
|
83
|
+
const captions = (locales ?? []).flatMap((locale) => {
|
|
84
|
+
const path = mediaCaptionPath(media, locale);
|
|
85
|
+
if (!path) return [];
|
|
86
|
+
const label = CaptionLocaleLabel[locale] ?? locale;
|
|
87
|
+
return [{ locale, label, src: buildMediaUrl(baseUrl, path, { token: viewerToken }) }];
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
src: buildMediaUrl(baseUrl, mediaPath(media), { token: viewerToken }),
|
|
91
|
+
token: viewerToken,
|
|
92
|
+
captions,
|
|
93
|
+
sessionSecret: media.visibility === "private" ? sessionSecret : void 0
|
|
94
|
+
};
|
|
95
|
+
}, [media, baseUrl, token, locales, sessionSecret]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export {
|
|
99
|
+
DevulturCapabilityError,
|
|
100
|
+
DevulturContext,
|
|
101
|
+
DevulturApiContext,
|
|
102
|
+
useDevultur,
|
|
103
|
+
useDevulturOptional,
|
|
104
|
+
useDevulturApi,
|
|
105
|
+
DevulturProgressContext,
|
|
106
|
+
useDevulturProgress,
|
|
107
|
+
useRequestTokenWhen,
|
|
108
|
+
useMediaUrl,
|
|
109
|
+
useVideoProps
|
|
110
|
+
};
|
|
@@ -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/component/http.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isBearerOf } from "@devultur/core";
|
|
2
2
|
import { httpRouter } from "convex/server";
|
|
3
|
-
import { internal } from "./_generated/api";
|
|
4
|
-
import { env, httpAction } from "./_generated/server";
|
|
3
|
+
import { internal } from "./_generated/api.js";
|
|
4
|
+
import { env, httpAction } from "./_generated/server.js";
|
|
5
5
|
const http = httpRouter();
|
|
6
6
|
http.route({
|
|
7
7
|
path: "/webhook",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type PaginationOptions, type PaginationResult } from "convex/server";
|
|
2
|
-
import type { Doc } from "./_generated/dataModel";
|
|
3
|
-
import type { QueryCtx } from "./_generated/server";
|
|
2
|
+
import type { Doc } from "./_generated/dataModel.js";
|
|
3
|
+
import type { QueryCtx } from "./_generated/server.js";
|
|
4
4
|
type MediaType = Doc<"media">["type"];
|
|
5
5
|
export declare const BACKFILL_BATCH_SIZE: {
|
|
6
6
|
readonly default: 100;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// records from before 1.4.0 the `searchText` they were written without.
|
|
3
3
|
import { paginationOptsValidator } from "convex/server";
|
|
4
4
|
import { v } from "convex/values";
|
|
5
|
-
import { internal } from "./_generated/api";
|
|
6
|
-
import { internalMutation, mutation, query } from "./_generated/server";
|
|
7
|
-
import { missingSearchText } from "./mediaMeta";
|
|
5
|
+
import { internal } from "./_generated/api.js";
|
|
6
|
+
import { internalMutation, mutation, query } from "./_generated/server.js";
|
|
7
|
+
import { missingSearchText } from "./mediaMeta.js";
|
|
8
8
|
const mediaType = v.union(v.literal("video"), v.literal("image"), v.literal("file"));
|
|
9
9
|
/** Longest search kept: Convex matches at most 16 terms, so a longer query only costs. */
|
|
10
10
|
const SEARCH_MAX_LENGTH = 200;
|
package/dist/component/lock.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// `lock_privilege_required`, `lock_conflict`, ...); see workerErrors.ts.
|
|
6
6
|
import { HOLD_PAGE_SIZE } from "@devultur/core";
|
|
7
7
|
import { v } from "convex/values";
|
|
8
|
-
import { derivativesValidator, retentionModeValidator } from "./lockValidators";
|
|
9
|
-
import { getMediaClient } from "./mediaClient";
|
|
10
|
-
import { workerAction } from "./workerErrors";
|
|
8
|
+
import { derivativesValidator, retentionModeValidator } from "./lockValidators.js";
|
|
9
|
+
import { getMediaClient } from "./mediaClient.js";
|
|
10
|
+
import { workerAction } from "./workerErrors.js";
|
|
11
11
|
const keysValidator = v.array(v.string());
|
|
12
12
|
export const getLock = workerAction({
|
|
13
13
|
args: { key: v.string() },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The component's client of the media Worker, from the env the app mounts the component with.
|
|
2
2
|
import { createWorkerClient } from "@devultur/core";
|
|
3
|
-
import { env } from "./_generated/server";
|
|
3
|
+
import { env } from "./_generated/server.js";
|
|
4
4
|
export function getMediaClient() {
|
|
5
5
|
return createWorkerClient(env.DEVULTUR_SECRET, env.DEVULTUR_API_URL);
|
|
6
6
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Editing a media record's descriptive metadata (title, alt text, tags). The upload's own facts (size, dimensions,
|
|
2
2
|
// duration, page count) are recorded by `uploads.ts` and the transcode webhook, never edited here.
|
|
3
3
|
import { v } from "convex/values";
|
|
4
|
-
import { mutation } from "./_generated/server";
|
|
5
|
-
import { mediaMetaPatch, searchTextOf } from "./mediaMeta";
|
|
4
|
+
import { mutation } from "./_generated/server.js";
|
|
5
|
+
import { mediaMetaPatch, searchTextOf } from "./mediaMeta.js";
|
|
6
6
|
/**
|
|
7
7
|
* Sets a record's `title`, `alt` and/or `tags` (only the ones passed). Trimmed; an empty title or alt clears it, and
|
|
8
8
|
* tags are deduplicated. Throws `invalid_media_metadata` past the limits (title 200, alt 1000, 20 tags of 50). Returns
|
package/dist/component/public.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { DEFAULT_TOKEN_EXPIRY_SECONDS, extractId, MediaEventName, MediaStatus } from "@devultur/core";
|
|
2
2
|
import { paginationOptsValidator } from "convex/server";
|
|
3
3
|
import { v } from "convex/values";
|
|
4
|
-
import { api, internal } from "./_generated/api";
|
|
5
|
-
import { internalMutation, mutation, query } from "./_generated/server";
|
|
6
|
-
import { listNewestFirst } from "./library";
|
|
7
|
-
import { objectLockValidator } from "./lockValidators";
|
|
8
|
-
import { getMediaClient } from "./mediaClient";
|
|
9
|
-
import { missingSearchText, searchTextOf } from "./mediaMeta";
|
|
10
|
-
import { transcodeOutputFields } from "./schema";
|
|
11
|
-
import { UPLOAD_CONFIRM_BACKSTOP_MS } from "./uploads";
|
|
12
|
-
import { workerAction } from "./workerErrors";
|
|
4
|
+
import { api, internal } from "./_generated/api.js";
|
|
5
|
+
import { internalMutation, mutation, query } from "./_generated/server.js";
|
|
6
|
+
import { listNewestFirst } from "./library.js";
|
|
7
|
+
import { objectLockValidator } from "./lockValidators.js";
|
|
8
|
+
import { getMediaClient } from "./mediaClient.js";
|
|
9
|
+
import { missingSearchText, searchTextOf } from "./mediaMeta.js";
|
|
10
|
+
import { transcodeOutputFields } from "./schema.js";
|
|
11
|
+
import { UPLOAD_CONFIRM_BACKSTOP_MS } from "./uploads.js";
|
|
12
|
+
import { workerAction } from "./workerErrors.js";
|
|
13
13
|
// Webhook path RELATIVE to the component. Convex gives the component a `CONVEX_SITE_URL` that ALREADY
|
|
14
14
|
// includes its mount prefix (e.g. `${site}/devultur`), so `${CONVEX_SITE_URL}/webhook` resolves to the
|
|
15
15
|
// mounted route (`${site}/devultur/webhook`) without duplicating the prefix. Do NOT add `/devultur/` here
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { extractId, sanitizeFilename } from "@devultur/core";
|
|
2
2
|
import { ConvexError, v } from "convex/values";
|
|
3
|
-
import { internal } from "./_generated/api";
|
|
4
|
-
import { internalAction, internalMutation } from "./_generated/server";
|
|
5
|
-
import { getMediaClient } from "./mediaClient";
|
|
6
|
-
import { searchTextOf, validUploadHints } from "./mediaMeta";
|
|
7
|
-
import { parseUploadKey } from "./uploadKeys";
|
|
8
|
-
import { workerAction } from "./workerErrors";
|
|
3
|
+
import { internal } from "./_generated/api.js";
|
|
4
|
+
import { internalAction, internalMutation } from "./_generated/server.js";
|
|
5
|
+
import { getMediaClient } from "./mediaClient.js";
|
|
6
|
+
import { searchTextOf, validUploadHints } from "./mediaMeta.js";
|
|
7
|
+
import { parseUploadKey } from "./uploadKeys.js";
|
|
8
|
+
import { workerAction } from "./workerErrors.js";
|
|
9
9
|
// Past the Worker's longest upload window (a multipart upload can run for 24 hours), so the scheduled check never runs
|
|
10
10
|
// while the bytes are still on their way.
|
|
11
11
|
export const UPLOAD_CONFIRM_BACKSTOP_MS = 25 * 60 * 60 * 1000;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Value } from "convex/values";
|
|
2
|
-
import { action } from "./_generated/server";
|
|
2
|
+
import { action } from "./_generated/server.js";
|
|
3
3
|
/** `data` of the `ConvexError` a component action throws when the Worker refuses a request. */
|
|
4
4
|
export interface DevulturErrorData {
|
|
5
5
|
/** The Worker's stable code (`retention_active`, `file_too_large`, ...), or `worker_error` when it sent none. */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// (`retention_active`, `lock_conflict`, `compliance_extend_only`, ...) and `error.data.details`.
|
|
5
5
|
import { DevulturApiError } from "@devultur/core";
|
|
6
6
|
import { ConvexError } from "convex/values";
|
|
7
|
-
import { action } from "./_generated/server";
|
|
7
|
+
import { action } from "./_generated/server.js";
|
|
8
8
|
function isApiError(error) {
|
|
9
9
|
if (error instanceof DevulturApiError)
|
|
10
10
|
return true;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DevulturMedia, MediaUrlOptions, Visibility } from '@devultur/core';
|
|
3
|
+
import { UploadProgress } from '@devultur/react';
|
|
4
|
+
import { FunctionReference } from 'convex/server';
|
|
5
|
+
import { Value } from 'convex/values';
|
|
6
|
+
|
|
7
|
+
/** What a viewer token action returns: the token with its lifetime, or the bare token (default lifetime). */
|
|
8
|
+
type ViewerTokenResult = {
|
|
9
|
+
token: string;
|
|
10
|
+
expiresIn: number;
|
|
11
|
+
} | string;
|
|
12
|
+
/** Extra arguments sent with `sessionSecret` to the viewer token action. */
|
|
13
|
+
type ViewerTokenArgs = Record<string, Value>;
|
|
14
|
+
/**
|
|
15
|
+
* Where the viewer token for the current `tokenArgs` stands: not requested yet, being issued (no token yet), held, or
|
|
16
|
+
* failed (the action threw or returned no token). A renewal in flight, or one that failed, keeps `"ready"`.
|
|
17
|
+
*/
|
|
18
|
+
type TokenStatus = "idle" | "loading" | "ready" | "error";
|
|
19
|
+
|
|
20
|
+
/** Transform/delivery options for media URLs (without `token`, which the context injects). */
|
|
21
|
+
type MediaRenderOptions = Omit<MediaUrlOptions, "token">;
|
|
22
|
+
/**
|
|
23
|
+
* The viewer token action: the component's `issueViewerToken` (as `defineDevulturFunctions` or `exposeApi` export it,
|
|
24
|
+
* where `sessionSecret` is optional) or an app's own, which may require it. The providers always send it.
|
|
25
|
+
*/
|
|
26
|
+
type ViewerTokenAction = FunctionReference<"action", "public", {
|
|
27
|
+
sessionSecret?: string;
|
|
28
|
+
}>;
|
|
29
|
+
interface MediaSession {
|
|
30
|
+
token: string | null;
|
|
31
|
+
/** Where the viewer token stands; `"error"` makes private previews fail fast with `token_unavailable`. */
|
|
32
|
+
tokenStatus: TokenStatus;
|
|
33
|
+
/** Why the last issuance failed (the action threw or returned no token), while `tokenStatus` is `"error"`. */
|
|
34
|
+
tokenError: Error | null;
|
|
35
|
+
/** Issues the viewer token again (after a failure, for example). Resolves to the new token, or `null`. */
|
|
36
|
+
retryToken: () => Promise<string | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Asks for the viewer token where it's issued lazily (`<DevulturViewerProvider lazyToken>`): the first private media
|
|
39
|
+
* shown calls it, and so must code that reads `token` itself. A no-op once asked, and where the token is issued
|
|
40
|
+
* right away (the default).
|
|
41
|
+
*/
|
|
42
|
+
requestToken: () => void;
|
|
43
|
+
baseUrl: string | null;
|
|
44
|
+
/** Browser session secret → sent in `${SESSION_HEADER}` on every private delivery. */
|
|
45
|
+
sessionSecret: string;
|
|
46
|
+
/** Delivery URL from a reference (non-hook → usable in handlers and `.map`). Token-transparent. */
|
|
47
|
+
url: (media: DevulturMedia, options?: MediaRenderOptions) => string;
|
|
48
|
+
/** Downloads a video's subtitle text for a locale (auth-transparent). */
|
|
49
|
+
getCaptionsText: (media: DevulturMedia, locale: string) => Promise<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Re-issues the viewer token (new session fingerprint with the current UA/IP) and updates the context.
|
|
52
|
+
* Single-flight: concurrent calls share the same issuance. Returns the new token (or null if it
|
|
53
|
+
* failed). The render uses it to transparently recover from a 401 `session_mismatch`.
|
|
54
|
+
*/
|
|
55
|
+
reissueToken: () => Promise<string | null>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type AnyAction = FunctionReference<"action">;
|
|
59
|
+
type AnyMutation = FunctionReference<"mutation">;
|
|
60
|
+
type AnyQuery = FunctionReference<"query">;
|
|
61
|
+
interface DevulturApi {
|
|
62
|
+
issueViewerToken: AnyAction;
|
|
63
|
+
createUploadUrl: AnyAction;
|
|
64
|
+
/** Records an upload in the component after its PUT, so it shows in `listMedia`. Optional (uploads still work). */
|
|
65
|
+
confirmUpload?: AnyAction;
|
|
66
|
+
deleteMedia: AnyAction;
|
|
67
|
+
/** Deletes the full folder of a video (raw/hls/thumbnails/captions). Required to delete videos. */
|
|
68
|
+
deleteVideo?: AnyAction;
|
|
69
|
+
/** Requests the transcode of an uploaded video. Required by `useMediaUpload` for auto-process. */
|
|
70
|
+
processVideo?: AnyAction;
|
|
71
|
+
getMedia?: AnyQuery;
|
|
72
|
+
/** The library, newest first. Required by `useMediaLibrary` and `<MediaLibrary>`. */
|
|
73
|
+
listMediaPage?: AnyQuery;
|
|
74
|
+
/** The library matching a search. Required by `useMediaLibrary` and `<MediaLibrary>` to search. */
|
|
75
|
+
searchMedia?: AnyQuery;
|
|
76
|
+
saveProgress?: AnyMutation;
|
|
77
|
+
loadProgress?: AnyQuery;
|
|
78
|
+
markComplete?: AnyMutation;
|
|
79
|
+
resetProgress?: AnyMutation;
|
|
80
|
+
}
|
|
81
|
+
interface UploadOptions {
|
|
82
|
+
/**
|
|
83
|
+
* Visibility override for this resource. Omitted → `config.upload.visibility` on your server, else the Worker's
|
|
84
|
+
* default by type. Your guard can still rewrite it.
|
|
85
|
+
*/
|
|
86
|
+
visibility?: Visibility;
|
|
87
|
+
/** Progress of the PUT: bytes sent, total bytes and their fraction (0 to 1). */
|
|
88
|
+
onProgress?: (progress: UploadProgress) => void;
|
|
89
|
+
/** Aborts the PUT → `upload()` rejects with an `AbortError`. */
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
/** A PDF's page count to record with it, a hint (`countPdfPages` from `@devultur/convex/pdfjs` counts it). */
|
|
92
|
+
pageCount?: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The reference `upload()` resolves to: a `DevulturMedia` whose `key` is always set (the uploaded object's key; a
|
|
96
|
+
* video's raw file, what `processVideo` takes). It stores as-is in a `dv.media()` field.
|
|
97
|
+
*/
|
|
98
|
+
type UploadedMedia = DevulturMedia & {
|
|
99
|
+
readonly key: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* What `useDevultur()` returns. Under a `<DevulturViewerProvider>`, `uploadUrl`, `upload` and `deleteMedia` throw a
|
|
103
|
+
* `DevulturCapabilityError` (`capability_unavailable`).
|
|
104
|
+
*/
|
|
105
|
+
interface DevulturContextValue extends MediaSession {
|
|
106
|
+
uploadUrl: (file: File) => Promise<{
|
|
107
|
+
url: string;
|
|
108
|
+
key: string;
|
|
109
|
+
}>;
|
|
110
|
+
/** Uploads a file (reporting progress), records it in the component, and returns the reference ready to save. */
|
|
111
|
+
upload: (file: File, options?: UploadOptions) => Promise<UploadedMedia>;
|
|
112
|
+
/** Deletes a media by reference: video → full folder; image/file → the object. */
|
|
113
|
+
deleteMedia: (media: DevulturMedia) => void;
|
|
114
|
+
}
|
|
115
|
+
declare const DevulturContext: React.Context<DevulturContextValue | null>;
|
|
116
|
+
declare const DevulturApiContext: React.Context<DevulturApi | null>;
|
|
117
|
+
declare function useDevultur(): DevulturContextValue;
|
|
118
|
+
/**
|
|
119
|
+
* `useDevultur()` for components that also render outside a provider: the same value under `<DevulturProvider>` or
|
|
120
|
+
* `<DevulturViewerProvider>`, and `null` outside both instead of throwing.
|
|
121
|
+
*/
|
|
122
|
+
declare function useDevulturOptional(): DevulturContextValue | null;
|
|
123
|
+
/**
|
|
124
|
+
* The `api` passed to `<DevulturProvider>`: your app's exported Devultur functions. Under a `<DevulturViewerProvider>`
|
|
125
|
+
* it throws a `DevulturCapabilityError`.
|
|
126
|
+
*/
|
|
127
|
+
declare function useDevulturApi(): DevulturApi;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Ready-to-use delivery URL for a key, with the context's viewer token already injected. The consumer
|
|
131
|
+
* never touches the token or the query — it only declares what it wants (download, resize, etc.).
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* <img src={useMediaUrl(key)} />
|
|
135
|
+
* <a href={useMediaUrl(key, { download: true })} download>Download</a>
|
|
136
|
+
* <img src={useMediaUrl(key, { width: 512, format: "webp" })} />
|
|
137
|
+
*/
|
|
138
|
+
declare function useMediaUrl(key: string | null | undefined, options?: MediaRenderOptions): string | null;
|
|
139
|
+
interface VideoProps {
|
|
140
|
+
src: string;
|
|
141
|
+
token: string | undefined;
|
|
142
|
+
captions: {
|
|
143
|
+
locale: string;
|
|
144
|
+
label: string;
|
|
145
|
+
src: string;
|
|
146
|
+
}[];
|
|
147
|
+
/** Session secret → `<VideoPlayer>` sends it in `${SESSION_HEADER}` on hls.js/captions requests. */
|
|
148
|
+
sessionSecret?: string;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Token-transparent props for `<VideoPlayer>` from a video reference: builds the HLS playlist `src`,
|
|
152
|
+
* the viewer `token` (only if private), and the `captions` for the requested locales.
|
|
153
|
+
* The consumer just does `<VideoPlayer {...useVideoProps(lesson.video, { locales })} />`.
|
|
154
|
+
*/
|
|
155
|
+
declare function useVideoProps(media: DevulturMedia | null | undefined, options?: {
|
|
156
|
+
locales?: string[];
|
|
157
|
+
}): VideoProps | null;
|
|
158
|
+
|
|
159
|
+
interface DevulturProgressContextValue {
|
|
160
|
+
save: (videoId: string, position: number) => Promise<void>;
|
|
161
|
+
load: (videoId: string) => Promise<{
|
|
162
|
+
lastPosition: number;
|
|
163
|
+
completedAt: Date | null;
|
|
164
|
+
} | null>;
|
|
165
|
+
markComplete: (videoId: string) => Promise<void>;
|
|
166
|
+
reset: (videoId: string) => Promise<void>;
|
|
167
|
+
}
|
|
168
|
+
declare const DevulturProgressContext: React.Context<DevulturProgressContextValue | null>;
|
|
169
|
+
declare function useDevulturProgress(): DevulturProgressContextValue;
|
|
170
|
+
|
|
171
|
+
export { type DevulturApi as D, type TokenStatus as T, type UploadOptions as U, type VideoProps as V, type ViewerTokenAction as a, type ViewerTokenArgs as b, DevulturApiContext as c, DevulturContext as d, type DevulturContextValue as e, DevulturProgressContext as f, type DevulturProgressContextValue as g, type UploadedMedia as h, type ViewerTokenResult as i, useDevulturApi as j, useDevulturOptional as k, useDevulturProgress as l, useMediaUrl as m, useVideoProps as n, useDevultur as u };
|