@foir/sdk 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +805 -4
  2. package/README.md +46 -15
  3. package/dist/auth/auth.d.ts +104 -1
  4. package/dist/auth/index.d.ts +12 -6
  5. package/dist/auth/operations.d.ts +85 -64
  6. package/dist/auth/ops.d.ts +99 -0
  7. package/dist/auth.js +775 -106
  8. package/dist/chunk-IGNLWT3H.js +35 -0
  9. package/dist/chunk-OHYVKTGB.js +45 -0
  10. package/dist/chunk-TAL5H6VA.js +39 -0
  11. package/dist/client.js +25 -31
  12. package/dist/codegen/block-registry.d.ts +13 -19
  13. package/dist/codegen/block-registry.js +3 -31
  14. package/dist/codegen/block-scaffold.d.ts +12 -21
  15. package/dist/codegen/block-scaffold.js +3 -41
  16. package/dist/codegen/index.d.ts +9 -4
  17. package/dist/codegen/index.js +7 -11
  18. package/dist/codegen/item-registry.d.ts +28 -0
  19. package/dist/codegen/item-registry.js +7 -0
  20. package/dist/codegen/item-scaffold.d.ts +29 -0
  21. package/dist/codegen/item-scaffold.js +7 -0
  22. package/dist/codegen/preset.d.ts +14 -7
  23. package/dist/codegen/shared.d.ts +20 -9
  24. package/dist/content/content-renderer.d.ts +90 -24
  25. package/dist/content/index.d.ts +13 -7
  26. package/dist/content/richtext.d.ts +66 -18
  27. package/dist/content.js +227 -80
  28. package/dist/data/builtins.d.ts +302 -92
  29. package/dist/data/client.d.ts +97 -25
  30. package/dist/data/document.d.ts +251 -0
  31. package/dist/data/index.d.ts +38 -19
  32. package/dist/data/manifest.d.ts +266 -3
  33. package/dist/data/schema.d.ts +78 -8
  34. package/dist/data/selection.d.ts +231 -37
  35. package/dist/data/subjects.d.ts +221 -0
  36. package/dist/data/subscription-document.d.ts +75 -0
  37. package/dist/data/subscriptions.d.ts +181 -0
  38. package/dist/data/transport.d.ts +255 -37
  39. package/dist/data/ws-transport.d.ts +197 -0
  40. package/dist/data.js +1419 -45
  41. package/dist/graphql/cdn-route.d.ts +12 -28
  42. package/dist/media/index.d.ts +6 -2
  43. package/dist/media/media.d.ts +2 -2
  44. package/dist/media/screen-capture.d.ts +81 -0
  45. package/dist/media.js +97 -0
  46. package/dist/next.js +663 -182
  47. package/dist/push.js +700 -90
  48. package/dist/react/FoirFeedback.d.ts +62 -0
  49. package/dist/react/feedback-notes.d.ts +101 -0
  50. package/dist/react/index.d.ts +7 -1
  51. package/dist/react.js +1272 -194
  52. package/dist/realtime.js +1 -1
  53. package/dist/server/events-client.d.ts +279 -0
  54. package/dist/server/index.d.ts +4 -0
  55. package/dist/server.js +298 -0
  56. package/dist/transport/foir-http.d.ts +23 -6
  57. package/dist/transport/persisted-query.d.ts +68 -0
  58. package/dist/transport/retry.d.ts +1 -1
  59. package/package.json +13 -1
  60. package/dist/chunk-ADO5FJNM.js +0 -30
@@ -1,5 +1,17 @@
1
1
  import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
2
2
  import { type FoirEnv } from '../transport/foir-http';
3
+ /**
4
+ * The shared read lane, for `@foir/sdk/client`.
5
+ *
6
+ * The mechanics — the URL, the hash, the handshake codes — moved to
7
+ * `../transport/persisted-query`, which has no `graphql` import, so
8
+ * `@foir/sdk/data` can build the same requests without dragging the parser into
9
+ * a storefront bundle. They are re-exported unchanged; this module's public
10
+ * surface is exactly what it always was. What stayed here is the part that
11
+ * needs a parsed document: `print`, and the eligibility rule that reads the
12
+ * printed text.
13
+ */
14
+ export { buildCdnUrl, documentHash, hasFoirErrorCode, NOT_SHAREABLE, PERSISTED_QUERY_NOT_FOUND, persistedQueryExtension, } from '../transport/persisted-query';
3
15
  /**
4
16
  * Whether this read can be served by the shared endpoint.
5
17
  *
@@ -10,33 +22,5 @@ import { type FoirEnv } from '../transport/foir-http';
10
22
  * saved.
11
23
  */
12
24
  export declare function isCdnEligible(env: FoirEnv, documentText: string, bearerToken?: string): boolean;
13
- /**
14
- * Build the shared-endpoint URL for a read.
15
- *
16
- * The credential goes in the URL deliberately: a shared cache keys on the URL
17
- * alone, so a key that rode only in a header would let two callers with
18
- * different reach collide on one entry and be served each other's body.
19
- */
20
- export declare function buildCdnUrl(endpoint: string, apiKey: string, hash: string, variables?: unknown): string;
21
- /** The APQ handshake object, in Apollo's shape because every client uses it. */
22
- export declare function persistedQueryExtension(hash: string): {
23
- persistedQuery: {
24
- version: number;
25
- sha256Hash: string;
26
- };
27
- };
28
- /**
29
- * SHA-256 of the document text, hex encoded — the identity a persisted query is
30
- * stored and retrieved under.
31
- *
32
- * Web Crypto rather than a bundled implementation: it is present in every
33
- * runtime this SDK targets (workers, edge, Node 18+) and adds nothing to the
34
- * caller's bundle.
35
- */
36
- export declare function documentHash(documentText: string): Promise<string>;
37
- /** The code the server answers with when it has never seen this hash. */
38
- export declare const PERSISTED_QUERY_NOT_FOUND = "PERSISTED_QUERY_NOT_FOUND";
39
- /** The code the shared endpoint refuses with, so a caller knows to retry. */
40
- export declare const NOT_SHAREABLE = "NOT_SHAREABLE";
41
25
  /** Print a document once, so eligibility and the URL agree on the same text. */
42
26
  export declare function printDocument<TData, TVariables>(document: TypedDocumentNode<TData, TVariables>): string;
@@ -1,8 +1,10 @@
1
1
  /**
2
- * `@foir/sdk/media` — progressive media components + Foir media adapters.
2
+ * `@foir/sdk/media` — progressive media components, Foir media adapters, and
3
+ * screen capture.
3
4
  *
4
5
  * `react` is a peer dependency; `hls.js` and `blurhash` are optional peers
5
- * loaded lazily (video/blurhash gracefully no-op when absent).
6
+ * loaded lazily (video/blurhash gracefully no-op when absent). The capture
7
+ * helpers are browser-only and pull in neither.
6
8
  */
7
9
  export { ProgressiveMedia } from './ProgressiveMedia';
8
10
  export type { ProgressiveMediaProps } from './ProgressiveMedia';
@@ -12,3 +14,5 @@ export { useProgressiveLoad } from './useProgressiveLoad';
12
14
  export type { LoadStage, UseProgressiveLoadOptions } from './useProgressiveLoad';
13
15
  export { toProgressiveImage, toProgressiveVideo, effectiveImageDimensions, cropAspectRatio, DEFAULT_VARIANT_TIERS, } from './media';
14
16
  export type { VariantTier, VariantSet, VariantTierKey, ProgressiveImageSource, ProgressiveVideoSource, FoirImageInput, FoirVideoInput, } from './media';
17
+ export { captureFoirScreen, isFoirScreenCaptureSupported } from './screen-capture';
18
+ export type { FoirScreenCapture, FoirScreenCaptureOptions } from './screen-capture';
@@ -72,7 +72,7 @@ export interface ProgressiveVideoSource {
72
72
  duration?: number;
73
73
  }
74
74
  /**
75
- * Foir GraphQL `ImageValue`-like input — the shape a consumer's codegen
75
+ * Foir GraphQL `Image`-like input — the shape a consumer's codegen
76
76
  * produces for an image field (intrinsic dims + per-format variant sets +
77
77
  * crop as opaque JSON). Structurally compatible with the generated type.
78
78
  */
@@ -89,7 +89,7 @@ export interface FoirImageInput {
89
89
  [key: string]: unknown;
90
90
  } | null;
91
91
  }
92
- /** Foir GraphQL `VideoValue`-like input. */
92
+ /** Foir GraphQL `Video`-like input. */
93
93
  export interface FoirVideoInput {
94
94
  url?: string | null;
95
95
  width?: number | null;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Screen capture — a picture of what the reader was looking at.
3
+ *
4
+ * ---------------------------------------------------------------------------
5
+ * Why it lives in the SDK
6
+ * ---------------------------------------------------------------------------
7
+ *
8
+ * Two surfaces need the same picture: the console's own bug report, and the
9
+ * feedback widget a customer's visitors use (`@foir/sdk/react`). One of those
10
+ * ships to npm and the other does not, so the shared half has to sit in the
11
+ * published package — a copy in each would be two answers to "how big is a
12
+ * screenshot", free to drift, and the drift would only show up as an upload the
13
+ * storage quota measures differently from the preview.
14
+ *
15
+ * ---------------------------------------------------------------------------
16
+ * The browser's picker IS the consent gate
17
+ * ---------------------------------------------------------------------------
18
+ *
19
+ * `getDisplayMedia` cannot be called without a user gesture and cannot resolve
20
+ * without the person choosing a surface in the browser's own dialog. So there is
21
+ * nothing here that asks for permission separately, and nothing that could
22
+ * capture without the reader having picked what to hand over. Call this from a
23
+ * click handler and from nowhere else.
24
+ *
25
+ * The stream is stopped before this function returns, on every path — a capture
26
+ * that left the tracks live would leave the browser's "sharing your screen"
27
+ * indicator on after a single still frame.
28
+ */
29
+ /** How the still is encoded. Both have defaults; neither is read from anywhere else. */
30
+ export interface FoirScreenCaptureOptions {
31
+ /**
32
+ * Longest horizontal edge of the still, in pixels. Wider captures are scaled
33
+ * down proportionally; narrower ones are left alone rather than upscaled.
34
+ *
35
+ * Default 1600, which is a readable full-page shot of a 4K display and lands
36
+ * around 200-400kB as JPEG. It is a bound on the bytes a reader uploads over
37
+ * their own connection, not a quality target.
38
+ */
39
+ maxWidth?: number;
40
+ /** JPEG quality, 0 to 1. Default 0.7. */
41
+ quality?: number;
42
+ }
43
+ /** One captured still. */
44
+ export interface FoirScreenCapture {
45
+ /** The bytes, for upload. `size` on it is what the storage quota measures. */
46
+ blob: Blob;
47
+ /**
48
+ * The same bytes as a `data:` URL, for an `<img src>` preview.
49
+ *
50
+ * Both, rather than one plus a converter, because the two callers need
51
+ * different halves and neither should re-encode: an object URL has a lifetime
52
+ * to manage and a second `toDataURL` would be a second encode of one frame.
53
+ */
54
+ dataUrl: string;
55
+ /** The still's own dimensions, after any downscale. */
56
+ width: number;
57
+ height: number;
58
+ mimeType: string;
59
+ }
60
+ /**
61
+ * Whether this browser can capture at all.
62
+ *
63
+ * Ask before rendering a capture button. `getDisplayMedia` is absent on every
64
+ * iOS browser and on any page served over plain HTTP, and a button that can only
65
+ * produce an error message is worse than no button.
66
+ */
67
+ export declare function isFoirScreenCaptureSupported(): boolean;
68
+ /**
69
+ * Capture one still of a surface the reader picks.
70
+ *
71
+ * Returns `null` when they dismissed the picker — that is a decision, not a
72
+ * failure, and a caller that treated it as one would put an error in front of
73
+ * somebody who just changed their mind. Everything else throws.
74
+ *
75
+ * The one ambiguity worth stating: a page embedded in an iframe without
76
+ * `allow="display-capture"` is refused with the SAME `NotAllowedError` the
77
+ * dismissal produces, so it lands here as `null` too. The browser gives no way
78
+ * to tell the two apart, and guessing wrong in the other direction would mean an
79
+ * error toast on every cancel.
80
+ */
81
+ export declare function captureFoirScreen(options?: FoirScreenCaptureOptions): Promise<FoirScreenCapture | null>;
package/dist/media.js CHANGED
@@ -392,12 +392,109 @@ function ProgressiveMedia({
392
392
  }
393
393
  );
394
394
  }
395
+
396
+ // src/media/screen-capture.ts
397
+ var DEFAULT_MAX_WIDTH = 1600;
398
+ var DEFAULT_QUALITY = 0.7;
399
+ var CAPTURE_MIME_TYPE = "image/jpeg";
400
+ function isFoirScreenCaptureSupported() {
401
+ return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
402
+ }
403
+ async function captureFoirScreen(options = {}) {
404
+ if (!isFoirScreenCaptureSupported()) {
405
+ throw new Error(
406
+ "[foir] this browser cannot capture the screen: navigator.mediaDevices.getDisplayMedia is unavailable. It needs a secure context (https, or localhost) and is absent on iOS. Call isFoirScreenCaptureSupported() before offering capture."
407
+ );
408
+ }
409
+ const maxWidth = options.maxWidth ?? DEFAULT_MAX_WIDTH;
410
+ const quality = options.quality ?? DEFAULT_QUALITY;
411
+ let stream;
412
+ try {
413
+ stream = await navigator.mediaDevices.getDisplayMedia({
414
+ video: { displaySurface: "browser" }
415
+ });
416
+ } catch (err) {
417
+ if (isDismissal(err)) return null;
418
+ throw err;
419
+ }
420
+ try {
421
+ const video = document.createElement("video");
422
+ video.muted = true;
423
+ video.playsInline = true;
424
+ video.srcObject = stream;
425
+ await waitForMetadata(video);
426
+ await video.play();
427
+ const { videoWidth, videoHeight } = video;
428
+ if (!videoWidth || !videoHeight) {
429
+ throw new Error(
430
+ "[foir] the captured stream reported no dimensions, so there is no frame to draw. This is the browser ending the capture before the first frame arrived \u2014 retry it."
431
+ );
432
+ }
433
+ const scale = videoWidth > maxWidth ? maxWidth / videoWidth : 1;
434
+ const width = Math.round(videoWidth * scale);
435
+ const height = Math.round(videoHeight * scale);
436
+ const canvas = document.createElement("canvas");
437
+ canvas.width = width;
438
+ canvas.height = height;
439
+ const ctx = canvas.getContext("2d");
440
+ if (!ctx) {
441
+ throw new Error(
442
+ "[foir] this browser gave no 2d canvas context, so the captured frame cannot be encoded. Nothing was uploaded."
443
+ );
444
+ }
445
+ ctx.drawImage(video, 0, 0, width, height);
446
+ video.srcObject = null;
447
+ const dataUrl = canvas.toDataURL(CAPTURE_MIME_TYPE, quality);
448
+ return {
449
+ blob: dataUrlToBlob(dataUrl),
450
+ dataUrl,
451
+ width,
452
+ height,
453
+ mimeType: CAPTURE_MIME_TYPE
454
+ };
455
+ } finally {
456
+ for (const track of stream.getTracks()) track.stop();
457
+ }
458
+ }
459
+ function isDismissal(err) {
460
+ const name = err?.name;
461
+ return name === "NotAllowedError" || name === "AbortError";
462
+ }
463
+ function waitForMetadata(video) {
464
+ if (video.readyState >= 1) return Promise.resolve();
465
+ return new Promise((resolve, reject) => {
466
+ video.addEventListener("loadedmetadata", () => resolve(), { once: true });
467
+ video.addEventListener(
468
+ "error",
469
+ () => reject(
470
+ new Error("[foir] the captured stream failed to load before a frame could be read")
471
+ ),
472
+ { once: true }
473
+ );
474
+ });
475
+ }
476
+ function dataUrlToBlob(dataUrl) {
477
+ const comma = dataUrl.indexOf(",");
478
+ const header = comma === -1 ? "" : dataUrl.slice(0, comma);
479
+ if (comma === -1 || !header.includes(";base64")) {
480
+ throw new Error(
481
+ `[foir] the canvas produced a data URL this build cannot decode (${header || "no header"}). Expected a base64 data URL.`
482
+ );
483
+ }
484
+ const binary = atob(dataUrl.slice(comma + 1));
485
+ const bytes = new Uint8Array(binary.length);
486
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
487
+ const mimeType = header.slice("data:".length, header.indexOf(";"));
488
+ return new Blob([bytes], { type: mimeType || CAPTURE_MIME_TYPE });
489
+ }
395
490
  export {
396
491
  BlurhashCanvas,
397
492
  DEFAULT_VARIANT_TIERS,
398
493
  ProgressiveMedia,
494
+ captureFoirScreen,
399
495
  cropAspectRatio,
400
496
  effectiveImageDimensions,
497
+ isFoirScreenCaptureSupported,
401
498
  toProgressiveImage,
402
499
  toProgressiveVideo,
403
500
  useProgressiveLoad