@devultur/convex 1.5.0 → 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 CHANGED
@@ -114,8 +114,11 @@ export const { createUploadUrl, confirmUpload, issueViewerToken, getMedia, listM
114
114
  - `visibility: "internal"` registers them as internal functions, for your own actions and mutations to call.
115
115
  - `config` takes what `Devultur.exposeApi` does (transcode preset, caption locales, upload lock and default
116
116
  visibility).
117
- - Each function keeps its arguments and result types, so `api.devultur.confirmUpload` is typed as before and
118
- `<DevulturProvider api={api.devultur}>` takes them as it takes `exposeApi`'s.
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.
119
122
 
120
123
  > **`Devultur.exposeApi` has no auth.** It returns the same functions with no guard: anyone who can reach your Convex
121
124
  > deployment can create upload URLs, confirm uploads, list the whole library and delete media. It keeps working, but
@@ -273,6 +276,21 @@ function Avatar({ media, fallbackUrl }) {
273
276
  }
274
277
  ```
275
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
+
276
294
  ### When the viewer token can't be issued
277
295
 
278
296
  `useDevultur()` tells where the viewer token stands:
@@ -290,7 +308,8 @@ there until `retryToken()`: a failed issuance isn't retried on its own. Meanwhil
290
308
  receives it), and `<DownloadLink onDownloadError>` receives it on click. They load once `retryToken()` gets a token.
291
309
 
292
310
  `<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)).
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`).
294
313
  `<Preview metrics={false}>` turns them off. A private video shows `loading` until its viewer token is issued.
295
314
 
296
315
  `<Preview>`'s video re-issues the token once when the delivery answers the playlist or a segment with a 401
@@ -796,6 +815,8 @@ what's enforced and the caveats.
796
815
  ## Documentation
797
816
 
798
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`.
799
820
 
800
821
  ## License
801
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
+ };
@@ -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;
@@ -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
@@ -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 };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { ObjectLock, LockView, SetLockOptions, HoldPage, DevulturMedia } from '@devultur/core';
1
+ import { WorkerUploadResult, DeleteVideoResult, TranscodeJobResult, ObjectLock, LockView, SetLockOptions, HoldPage, DevulturMedia } from '@devultur/core';
2
2
  export { CaptionLocale, DevulturConfig, DevulturKind, DevulturMedia, ImageFit, ImageFormat, SecurityLevel, SpeechProvider, StorageProvider, TranscodePreset, TranscodeProvider, TranslationProvider, VideoModuleConfig, Visibility } from '@devultur/core';
3
- import { FunctionReference, FunctionVisibility, DefaultFunctionArgs, GenericQueryCtx, GenericDataModel, GenericMutationCtx, GenericActionCtx, RegisteredQuery, RegisteredMutation, RegisteredAction, PaginationOptions, PaginationResult } from 'convex/server';
3
+ import { FunctionReference, PaginationResult, FunctionVisibility, DefaultFunctionArgs, GenericQueryCtx, GenericDataModel, GenericMutationCtx, GenericActionCtx, RegisteredQuery, RegisteredMutation, RegisteredAction, PaginationOptions } from 'convex/server';
4
4
  import { PropertyValidators, Value, ConvexError } from 'convex/values';
5
- import { D as DevulturComponentConfig, M as MediaRecord } from './types-C0ruHUcu.js';
6
- export { a as DevulturUploadConfig, P as ProgressRecord } from './types-C0ruHUcu.js';
5
+ import { M as MediaRecord, P as ProgressRecord, D as DevulturComponentConfig } from './types-C0ruHUcu.js';
6
+ export { a as DevulturUploadConfig } 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
 
@@ -188,6 +188,42 @@ type ComponentApi<Name extends string | undefined = string | undefined> = {
188
188
  };
189
189
  };
190
190
 
191
+ /** Convex's system fields on a document a component returns. Its `_id` is a plain string in your app. */
192
+ interface ComponentSystemFields {
193
+ _id: string;
194
+ _creationTime: number;
195
+ }
196
+ /** A media record as the component's functions return it: the stored fields and the system fields. */
197
+ interface MediaDocument extends MediaRecord, ComponentSystemFields {
198
+ }
199
+ /** A watch-progress record as the component's functions return it. */
200
+ interface ProgressDocument extends ProgressRecord, ComponentSystemFields {
201
+ }
202
+ /** The result of each exposed function, by name. */
203
+ interface DevulturFunctionResults {
204
+ issueViewerToken: {
205
+ token: string;
206
+ expiresIn: number;
207
+ };
208
+ createUploadUrl: WorkerUploadResult;
209
+ /** The record, or `null` when nothing is stored at the key. */
210
+ confirmUpload: MediaDocument | null;
211
+ /** The updated record, or `null` when there is none. */
212
+ updateMediaMeta: MediaDocument | null;
213
+ deleteMedia: null;
214
+ deleteVideo: DeleteVideoResult;
215
+ processVideo: TranscodeJobResult;
216
+ getMedia: MediaDocument | null;
217
+ listMediaPage: PaginationResult<MediaDocument>;
218
+ searchMedia: PaginationResult<MediaDocument>;
219
+ /** The progress record's id. */
220
+ saveProgress: string;
221
+ loadProgress: ProgressDocument | null;
222
+ /** The progress record's id. */
223
+ markComplete: string;
224
+ resetProgress: null;
225
+ }
226
+
191
227
  type FunctionKind = "query" | "mutation" | "action";
192
228
  type ContextOf<Kind extends FunctionKind> = Kind extends "query" ? GenericQueryCtx<GenericDataModel> : Kind extends "mutation" ? GenericMutationCtx<GenericDataModel> : GenericActionCtx<GenericDataModel>;
193
229
  /** What a guard tells the function about its caller. */
@@ -283,10 +319,10 @@ declare function exposedFunctions(component: ComponentApi, config?: DevulturComp
283
319
  };
284
320
  type ExposedFunctions = ReturnType<typeof exposedFunctions>;
285
321
  type ExposedFunctionName = keyof ExposedFunctions;
286
- type Registered<Fn, Visibility extends FunctionVisibility> = Fn extends ExposedFunction<"query", infer Args, infer Result> ? RegisteredQuery<Visibility, Args, Promise<Result>> : Fn extends ExposedFunction<"mutation", infer Args, infer Result> ? RegisteredMutation<Visibility, Args, Promise<Result>> : Fn extends ExposedFunction<"action", infer Args, infer Result> ? RegisteredAction<Visibility, Args, Promise<Result>> : never;
322
+ type Registered<Fn, Visibility extends FunctionVisibility, Result> = Fn extends ExposedFunction<"query", infer Args, unknown> ? RegisteredQuery<Visibility, Args, Promise<Result>> : Fn extends ExposedFunction<"mutation", infer Args, unknown> ? RegisteredMutation<Visibility, Args, Promise<Result>> : Fn extends ExposedFunction<"action", infer Args, unknown> ? RegisteredAction<Visibility, Args, Promise<Result>> : never;
287
323
  /** The Convex functions registered from `ExposedFunctions`, each with its own args and result. */
288
324
  type RegisteredFunctions<Visibility extends FunctionVisibility> = {
289
- [Name in ExposedFunctionName]: Registered<ExposedFunctions[Name], Visibility>;
325
+ [Name in ExposedFunctionName]: Registered<ExposedFunctions[Name], Visibility, DevulturFunctionResults[Name]>;
290
326
  };
291
327
 
292
328
  type DevulturComponent = ComponentApi;
@@ -329,34 +365,35 @@ declare class Devultur {
329
365
  /** The mounted component (`components.devultur`) this client calls. */
330
366
  readonly component: DevulturComponent;
331
367
  constructor(component: DevulturComponent);
332
- getMedia(ctx: QueryCtx, key: string): Promise<any>;
368
+ getMedia(ctx: QueryCtx, key: string): Promise<MediaDocument | null>;
369
+ /** Returns the progress record's id. */
333
370
  saveProgress(ctx: MutationCtx, args: {
334
371
  userId: string;
335
372
  videoId: string;
336
373
  position: number;
337
- }): Promise<any>;
374
+ }): Promise<string>;
338
375
  loadProgress(ctx: QueryCtx, args: {
339
376
  userId: string;
340
377
  videoId: string;
341
- }): Promise<any>;
378
+ }): Promise<ProgressDocument | null>;
342
379
  /** An upload URL from your server, optionally locked (retention and/or hold, written with the bytes). */
343
380
  createUploadUrl(ctx: ActionCtx, args: UploadUrlArgs & {
344
381
  lock?: ObjectLock;
345
382
  hold?: boolean;
346
- }): Promise<any>;
383
+ }): Promise<DevulturFunctionResults["createUploadUrl"]>;
347
384
  /**
348
385
  * Records an upload in the component once its bytes are stored (after the PUT): it then shows in `listMedia`.
349
386
  * Returns the record, or `null` when nothing is stored at `key`. Uploads nobody confirms are recorded a day later.
350
387
  * `width`, `height` and `pageCount` are the client's hints (positive integers up to 100,000); an image size the
351
388
  * Worker read from the bytes wins over them.
352
389
  */
353
- confirmUpload(ctx: ActionCtx, args: ConfirmUploadArgs): Promise<MediaRecord | null>;
390
+ confirmUpload(ctx: ActionCtx, args: ConfirmUploadArgs): Promise<MediaDocument | null>;
354
391
  /**
355
392
  * Sets a record's `title`, `alt` and/or `tags` (only the ones passed; an empty string clears a title or alt, `[]`
356
393
  * the tags). `key` is the media id. Trimmed and deduplicated; past title 200, alt 1000 or 20 tags of 50 characters
357
394
  * it throws `invalid_media_metadata`. Returns the updated record, or `null` when there is none.
358
395
  */
359
- updateMediaMeta(ctx: MutationCtx, args: MediaMetaArgs): Promise<MediaRecord | null>;
396
+ updateMediaMeta(ctx: MutationCtx, args: MediaMetaArgs): Promise<MediaDocument | null>;
360
397
  /**
361
398
  * The recorded media (every confirmed upload, and every video sent to `processVideo`), optionally of one type.
362
399
  * `mediaFromRecord(record)` turns a row into the reference `<Image>` and `dv.media()` take. Unbounded: it reads every
@@ -364,15 +401,15 @@ declare class Devultur {
364
401
  */
365
402
  listMedia(ctx: QueryCtx, args?: {
366
403
  type?: MediaType;
367
- }): Promise<MediaRecord[]>;
404
+ }): Promise<MediaDocument[]>;
368
405
  /** One page of the recorded media, newest first, optionally of one type. Takes Convex's `paginationOpts`. */
369
- listMediaPage(ctx: QueryCtx, args: MediaPageArgs): Promise<PaginationResult<MediaRecord>>;
406
+ listMediaPage(ctx: QueryCtx, args: MediaPageArgs): Promise<PaginationResult<MediaDocument>>;
370
407
  /**
371
408
  * One page of the records whose title or filename matches `search` (its last word as a prefix), best match first,
372
409
  * optionally of one type. A blank `search` lists newest first, like `listMediaPage`. Records from before 1.4.0
373
410
  * match once `backfillSearchText` (or any write) gives them a `searchText`.
374
411
  */
375
- searchMedia(ctx: QueryCtx, args: SearchMediaArgs): Promise<PaginationResult<MediaRecord>>;
412
+ searchMedia(ctx: QueryCtx, args: SearchMediaArgs): Promise<PaginationResult<MediaDocument>>;
376
413
  /**
377
414
  * One-off for records written before 1.4.0: gives every record without a `searchText` one, `batchSize` records
378
415
  * per mutation (default 100, at most 1000). Runs the first batch and schedules the rest. Safe to run again.
@@ -468,4 +505,4 @@ declare function defineDevulturFunctions<Visibility extends FunctionVisibility =
468
505
  */
469
506
  declare function mediaFromRecord(record: MediaRecord): DevulturMedia | null;
470
507
 
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 };
508
+ export { type CallerContext, type ComponentSystemFields, Devultur, type DevulturArgsRewrite, DevulturComponentConfig, type DevulturErrorData, type DevulturFunctionResults, type DevulturFunctionsOptions, type DevulturGuard, type DevulturGuardCtx, type DevulturGuardResult, type DevulturOperation, type MediaDocument, MediaRecord, type ProgressDocument, ProgressRecord, type RegisteredFunctions, defineDevulturFunctions, devulturErrorOf, isDevulturError, mediaFromRecord };
package/dist/index.js CHANGED
@@ -186,6 +186,7 @@ var Devultur = class {
186
186
  async getMedia(ctx, key) {
187
187
  return ctx.runQuery(this.component.public.getMediaByKey, { key });
188
188
  }
189
+ /** Returns the progress record's id. */
189
190
  async saveProgress(ctx, args) {
190
191
  return ctx.runMutation(this.component.public.saveProgress, args);
191
192
  }
@@ -0,0 +1,6 @@
1
+ export { D as DevulturApi, c as DevulturApiContext, d as DevulturContext, e as DevulturContextValue, f as DevulturProgressContext, g as DevulturProgressContextValue, U as UploadOptions, h as UploadedMedia, V as VideoProps, u as useDevultur, j as useDevulturApi, k as useDevulturOptional, l as useDevulturProgress, m as useMediaUrl, n as useVideoProps } from '../hooks-uqpBd5UF.js';
2
+ import 'react';
3
+ import '@devultur/core';
4
+ import '@devultur/react';
5
+ import 'convex/server';
6
+ import 'convex/values';