jazz-tools 0.18.26 → 0.18.27

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 (35) hide show
  1. package/.turbo/turbo-build.log +43 -43
  2. package/CHANGELOG.md +11 -0
  3. package/dist/index.js +96 -4
  4. package/dist/index.js.map +1 -1
  5. package/dist/media/{chunk-W3S526L3.js → chunk-K6GCHLQU.js} +1 -1
  6. package/dist/media/chunk-K6GCHLQU.js.map +1 -0
  7. package/dist/media/create-image/browser.d.ts +1 -1
  8. package/dist/media/create-image/react-native.d.ts +1 -1
  9. package/dist/media/create-image/react-native.d.ts.map +1 -1
  10. package/dist/media/create-image/server.d.ts +1 -1
  11. package/dist/media/create-image-factory.d.ts +5 -2
  12. package/dist/media/create-image-factory.d.ts.map +1 -1
  13. package/dist/media/index.browser.js +1 -1
  14. package/dist/media/index.d.ts +3 -4
  15. package/dist/media/index.d.ts.map +1 -1
  16. package/dist/media/index.js +1 -1
  17. package/dist/media/index.native.js +63 -28
  18. package/dist/media/index.native.js.map +1 -1
  19. package/dist/media/index.server.js +1 -1
  20. package/dist/react/index.js.map +1 -1
  21. package/dist/tools/coValues/request.d.ts +70 -0
  22. package/dist/tools/coValues/request.d.ts.map +1 -1
  23. package/dist/tools/exports.d.ts +1 -1
  24. package/dist/tools/exports.d.ts.map +1 -1
  25. package/dist/tools/tests/authenticate-request.test.d.ts +2 -0
  26. package/dist/tools/tests/authenticate-request.test.d.ts.map +1 -0
  27. package/package.json +8 -4
  28. package/src/media/create-image/react-native.ts +75 -30
  29. package/src/media/create-image-factory.test.ts +18 -0
  30. package/src/media/create-image-factory.ts +6 -1
  31. package/src/media/index.ts +7 -4
  32. package/src/tools/coValues/request.ts +188 -4
  33. package/src/tools/exports.ts +3 -0
  34. package/src/tools/tests/authenticate-request.test.ts +194 -0
  35. package/dist/media/chunk-W3S526L3.js.map +0 -1
@@ -37,6 +37,11 @@ export type CreateImageOptions = {
37
37
  progressive?: boolean;
38
38
  };
39
39
 
40
+ export type CreateImageReturnType = Loaded<
41
+ typeof ImageDefinition,
42
+ { original: true }
43
+ >;
44
+
40
45
  export type CreateImageImpl<
41
46
  TSourceType = SourceType,
42
47
  TResizeOutput = ResizeOutput,
@@ -70,7 +75,7 @@ async function createImage<TSourceType, TResizeOutput>(
70
75
  imageBlobOrFile: TSourceType,
71
76
  options: CreateImageOptions,
72
77
  impl: CreateImageImpl<TSourceType, TResizeOutput>,
73
- ): Promise<Loaded<typeof ImageDefinition, { $each: true }>> {
78
+ ): Promise<CreateImageReturnType> {
74
79
  // Get the original size of the image
75
80
  const { width: originalWidth, height: originalHeight } =
76
81
  await impl.getImageSize(imageBlobOrFile);
@@ -1,5 +1,8 @@
1
- import type { ImageDefinition } from "jazz-tools";
2
- import { CreateImageOptions } from "./create-image-factory";
1
+ import type { ImageDefinition, Loaded } from "jazz-tools";
2
+ import type {
3
+ CreateImageOptions,
4
+ CreateImageReturnType,
5
+ } from "./create-image-factory";
3
6
 
4
7
  export * from "./exports";
5
8
 
@@ -38,7 +41,7 @@ export * from "./exports";
38
41
  export declare function createImage(
39
42
  imageBlobOrFile: Blob | File,
40
43
  options?: CreateImageOptions,
41
- ): Promise<ImageDefinition>;
44
+ ): Promise<CreateImageReturnType>;
42
45
 
43
46
  /**
44
47
  * Creates an ImageDefinition from an image file path with built-in UX features.
@@ -66,4 +69,4 @@ export declare function createImage(
66
69
  export declare function createImage(
67
70
  filePath: string,
68
71
  options?: CreateImageOptions,
69
- ): Promise<ImageDefinition>;
72
+ ): Promise<CreateImageReturnType>;
@@ -172,13 +172,16 @@ async function serializeMessagePayload({
172
172
  };
173
173
  }
174
174
 
175
+ const coIdSchema = z.custom<`co_z${string}`>(isCoValueId);
176
+ const signatureSchema = z.custom<`signature_z${string}`>(
177
+ (value) => typeof value === "string" && value.startsWith("signature_z"),
178
+ );
179
+
175
180
  const requestSchema = z.object({
176
181
  contentPieces: z.array(z.json()),
177
- id: z.custom<`co_z${string}`>(isCoValueId),
182
+ id: coIdSchema,
178
183
  createdAt: z.number(),
179
- authToken: z.custom<`signature_z${string}`>(
180
- (value) => typeof value === "string" && value.startsWith("signature_z"),
181
- ),
184
+ authToken: signatureSchema,
182
185
  signerID: z.custom<`signer_z${string}`>(
183
186
  (value) => typeof value === "string" && value.startsWith("signer_z"),
184
187
  ),
@@ -631,3 +634,184 @@ async function loadWorkerAccountOrGroup(id: string, loadAs: Account) {
631
634
  loadAs,
632
635
  });
633
636
  }
637
+
638
+ function defaultGetToken(request: Request) {
639
+ const headerValue = request.headers.get("Authorization");
640
+ if (headerValue?.startsWith("Jazz ")) {
641
+ return headerValue.replace("Jazz ", "");
642
+ }
643
+
644
+ if (headerValue) {
645
+ console.warn(
646
+ "An Authorization header was found, but it did not start with 'Jazz '. If this is intentional, you can specify the location of the token using the `getToken` option.",
647
+ );
648
+ }
649
+
650
+ return undefined;
651
+ }
652
+
653
+ /**
654
+ * Authenticates a Request by verifying a signed authentication token.
655
+ *
656
+ * - If a token is not provided, the returned account is `undefined` and no error is returned.
657
+ * - If a valid token is provided, the signer account is returned.
658
+ * - If an invalid token is provided, an error is returned detailing the validation error, and the returned account is `undefined`.
659
+ *
660
+ * @see {@link generateAuthToken} for generating a token.
661
+ *
662
+ * Note: This function does not perform any authorization checks, it only verifies if - **when provided** - a token is valid. It is up to the caller to perform any additional authorization checks, if needed.
663
+ *
664
+ * @param request - The request to authenticate.
665
+ * @param options - The options for the authentication.
666
+ * @param options.expiration - The expiration time of the token in milliseconds, defaults to 1 minute.
667
+ * @param options.loadAs - The account to load the token from, defaults to the current active account.
668
+ * @param options.getToken - If specified, this function will be used to get the token from the request. By default the token is expected to be in the `Authorization` header in the form of `Jazz <token>`.
669
+ * @returns The account if it is valid, otherwise an error.
670
+ *
671
+ * @example
672
+ * ```ts
673
+ * const { account, error } = await authenticateRequest(request);
674
+ * if (error) {
675
+ * return new Response(JSON.stringify(error), { status: 401 });
676
+ * }
677
+ * ```
678
+ */
679
+ export async function authenticateRequest(
680
+ request: Request,
681
+ options?: {
682
+ expiration?: number;
683
+ loadAs?: Account;
684
+ getToken?: (request: Request) => string | undefined | null;
685
+ },
686
+ ): Promise<
687
+ | {
688
+ account?: Account;
689
+ error?: never;
690
+ }
691
+ | {
692
+ account?: never;
693
+ error: { message: string; details?: unknown };
694
+ }
695
+ > {
696
+ const token = options?.getToken?.(request) ?? defaultGetToken(request);
697
+
698
+ if (!token) {
699
+ return {};
700
+ }
701
+
702
+ const { account, error } = await parseAuthToken(token, {
703
+ loadAs: options?.loadAs,
704
+ expiration: options?.expiration ?? 1000 * 60,
705
+ });
706
+
707
+ if (error) {
708
+ return { error };
709
+ }
710
+
711
+ return { account, error };
712
+ }
713
+
714
+ /**
715
+ * Generates an authentication token for a given account. This token can be used to authenticate a request. See {@link authenticateRequest} for more details.
716
+ *
717
+ * @param as - The account to generate the token for, defaults to the current active account.
718
+ * @returns The authentication token.
719
+ *
720
+ * @example Make a fetch request with the token
721
+ * ```ts
722
+ * const token = generateAuthToken();
723
+ * const response = await fetch(url, {
724
+ * headers: {
725
+ * Authorization: `Jazz ${token}`,
726
+ * },
727
+ * });
728
+ * ```
729
+ */
730
+
731
+ export function generateAuthToken(as?: Account) {
732
+ const account = as ?? Account.getMe();
733
+ const node = account.$jazz.localNode;
734
+ const crypto = node.crypto;
735
+
736
+ const agent = node.getCurrentAgent();
737
+ const signerSecret = agent.currentSignerSecret();
738
+
739
+ const createdAt = Date.now();
740
+
741
+ const signPayload = crypto.secureHash({
742
+ id: account.$jazz.id,
743
+ createdAt,
744
+ });
745
+
746
+ const authToken = crypto.sign(signerSecret, signPayload);
747
+
748
+ return `${authToken}~${account.$jazz.id}~${createdAt}`;
749
+ }
750
+
751
+ export async function parseAuthToken(
752
+ authToken: string,
753
+ options?: { loadAs?: Account; expiration?: number },
754
+ ): Promise<
755
+ | { account: Account; error?: never }
756
+ | { account?: never; error: { message: string; details?: unknown } }
757
+ > {
758
+ const expiration = options?.expiration ?? 1_000 * 60; // 1 minute
759
+
760
+ const parsed = z
761
+ .tuple([signatureSchema, coIdSchema, z.string().transform(Number)])
762
+ .safeParse(authToken.split("~"));
763
+
764
+ if (!parsed.success) {
765
+ return {
766
+ error: {
767
+ message: "Invalid token",
768
+ details: parsed.error,
769
+ },
770
+ };
771
+ }
772
+
773
+ const [signature, id, createdAt] = parsed.data;
774
+
775
+ if (createdAt + expiration < Date.now()) {
776
+ return {
777
+ error: {
778
+ message: "Token expired",
779
+ },
780
+ };
781
+ }
782
+
783
+ const account = await Account.load(id, { loadAs: options?.loadAs });
784
+
785
+ if (!account) {
786
+ return {
787
+ error: {
788
+ message: "Failed to load account",
789
+ details: { id },
790
+ },
791
+ };
792
+ }
793
+
794
+ const node = account.$jazz.localNode;
795
+ const crypto = node.crypto;
796
+
797
+ // Verify the signature of the message to prevent tampering
798
+ const signPayload = crypto.secureHash({
799
+ id: account.$jazz.id,
800
+ createdAt: Number(createdAt),
801
+ });
802
+
803
+ const agentID = account.$jazz.raw.currentAgentID();
804
+ const signerID = crypto.getAgentSignerID(agentID);
805
+
806
+ if (!crypto.verify(signature, signPayload, signerID)) {
807
+ return {
808
+ error: {
809
+ message: "Invalid signature",
810
+ },
811
+ };
812
+ }
813
+
814
+ return {
815
+ account,
816
+ };
817
+ }
@@ -116,6 +116,9 @@ export {
116
116
  experimental_defineRequest,
117
117
  JazzRequestError,
118
118
  isJazzRequestError,
119
+ authenticateRequest,
120
+ generateAuthToken,
121
+ parseAuthToken,
119
122
  type HttpRoute,
120
123
  } from "./coValues/request.js";
121
124
 
@@ -0,0 +1,194 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { authenticateRequest, generateAuthToken } from "../coValues/request.js";
3
+ import { createJazzTestAccount } from "../testing.js";
4
+
5
+ afterEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+
9
+ describe("authenticateRequest", () => {
10
+ it("should correctly authenticate a request", async () => {
11
+ const me = await createJazzTestAccount({
12
+ isCurrentActiveAccount: true,
13
+ });
14
+
15
+ const token = generateAuthToken();
16
+
17
+ const { account, error } = await authenticateRequest(
18
+ new Request("https://api.example.com/api/user", {
19
+ headers: {
20
+ Authorization: `Jazz ${token}`,
21
+ },
22
+ }),
23
+ );
24
+
25
+ expect(error).toBeUndefined();
26
+ expect(account?.$jazz.id).toBe(me.$jazz.id);
27
+ });
28
+
29
+ it("should not return an account if no token is provided", async () => {
30
+ await createJazzTestAccount({
31
+ isCurrentActiveAccount: true,
32
+ });
33
+
34
+ const { account, error } = await authenticateRequest(
35
+ new Request("https://api.example.com/api/user", {}),
36
+ );
37
+
38
+ expect(error).toBeUndefined();
39
+ expect(account).toBeUndefined();
40
+ });
41
+
42
+ it("should return an error if the token is invalid", async () => {
43
+ const { account, error } = await authenticateRequest(
44
+ new Request("https://api.example.com/api/user", {
45
+ headers: {
46
+ Authorization: `Jazz invalid~invalid~invalid`,
47
+ },
48
+ }),
49
+ );
50
+
51
+ expect(error).toMatchObject(
52
+ expect.objectContaining({
53
+ message: "Invalid token",
54
+ details: expect.anything(),
55
+ }),
56
+ );
57
+ expect(account).toBeUndefined();
58
+ });
59
+
60
+ it("should return an error if the token is malformed", async () => {
61
+ const { account, error } = await authenticateRequest(
62
+ new Request("https://api.example.com/api/user", {
63
+ headers: {
64
+ Authorization: `Jazz malformed`,
65
+ },
66
+ }),
67
+ );
68
+
69
+ expect(error).toMatchObject(
70
+ expect.objectContaining({
71
+ message: "Invalid token",
72
+ details: expect.anything(),
73
+ }),
74
+ );
75
+ expect(account).toBeUndefined();
76
+ });
77
+
78
+ it("should be resilient to tampering", async () => {
79
+ await createJazzTestAccount({
80
+ isCurrentActiveAccount: true,
81
+ });
82
+
83
+ const token = generateAuthToken();
84
+ const tokenParts = token.split("~");
85
+ tokenParts[2] = "999999999999999";
86
+ const tamperedToken = tokenParts.join("~");
87
+
88
+ const { account, error } = await authenticateRequest(
89
+ new Request("https://api.example.com/api/user", {
90
+ headers: {
91
+ Authorization: `Jazz ${tamperedToken}`,
92
+ },
93
+ }),
94
+ );
95
+
96
+ expect(error).toMatchObject(
97
+ expect.objectContaining({
98
+ message: "Invalid signature",
99
+ }),
100
+ );
101
+ expect(account).toBeUndefined();
102
+ });
103
+
104
+ it("should return an error if the token is expired", async () => {
105
+ await createJazzTestAccount({
106
+ isCurrentActiveAccount: true,
107
+ });
108
+
109
+ const token = generateAuthToken();
110
+
111
+ const { account, error } = await authenticateRequest(
112
+ new Request("https://api.example.com/api/user", {
113
+ headers: {
114
+ Authorization: `Jazz ${token}`,
115
+ },
116
+ }),
117
+ {
118
+ expiration: -1000,
119
+ },
120
+ );
121
+
122
+ expect(error).toMatchObject(
123
+ expect.objectContaining({
124
+ message: "Token expired",
125
+ }),
126
+ );
127
+ expect(account).toBeUndefined();
128
+ });
129
+
130
+ it("should treat the request as unauthenticated if the token is not in the default format, even if present.", async () => {
131
+ vi.spyOn(console, "warn").mockImplementation(() => {});
132
+ await createJazzTestAccount({
133
+ isCurrentActiveAccount: true,
134
+ });
135
+
136
+ const token = generateAuthToken();
137
+
138
+ const { account, error } = await authenticateRequest(
139
+ new Request("https://api.example.com/api/user", {
140
+ headers: {
141
+ Authorization: `${token}`,
142
+ },
143
+ }),
144
+ );
145
+
146
+ expect(console.warn).toHaveBeenCalled();
147
+ expect(account).toBeUndefined();
148
+ expect(error).toBeUndefined();
149
+ });
150
+
151
+ it("should correctly validate a request when the token is in a non standard location", async () => {
152
+ const me = await createJazzTestAccount({
153
+ isCurrentActiveAccount: true,
154
+ });
155
+
156
+ const token = generateAuthToken();
157
+
158
+ const { account, error } = await authenticateRequest(
159
+ new Request("https://api.example.com/api/user", {
160
+ headers: {
161
+ ["x-jazz-auth-token"]: `${token}`,
162
+ },
163
+ }),
164
+ {
165
+ getToken: (request) => request.headers.get("x-jazz-auth-token"),
166
+ },
167
+ );
168
+
169
+ expect(error).toBeUndefined();
170
+ expect(account?.$jazz.id).toBe(me.$jazz.id);
171
+ });
172
+
173
+ it("should correctly validate a request when the token is generated from a non active account", async () => {
174
+ const notAnActiveAccount = await createJazzTestAccount({
175
+ isCurrentActiveAccount: false,
176
+ });
177
+
178
+ const token = generateAuthToken(notAnActiveAccount);
179
+
180
+ const { account, error } = await authenticateRequest(
181
+ new Request("https://api.example.com/api/user", {
182
+ headers: {
183
+ Authorization: `Jazz ${token}`,
184
+ },
185
+ }),
186
+ {
187
+ loadAs: notAnActiveAccount,
188
+ },
189
+ );
190
+
191
+ expect(error).toBeUndefined();
192
+ expect(account?.$jazz.id).toBe(notAnActiveAccount.$jazz.id);
193
+ });
194
+ });
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/media/utils.ts","../../src/media/create-image-factory.ts"],"sourcesContent":["import type { CoID } from \"cojson\";\nimport { Account, FileStream, ImageDefinition } from \"jazz-tools\";\n\nexport function highestResAvailable(\n image: ImageDefinition,\n wantedWidth: number,\n wantedHeight: number,\n): { width: number; height: number; image: FileStream } | null {\n const availableSizes: [number, number, string][] = image.$jazz.raw\n .keys()\n .filter((key) => /^\\d+x\\d+$/.test(key))\n .map((key) => {\n const [w, h] = key.split(\"x\").map(Number) as [number, number];\n return [w, h, key];\n });\n\n if (availableSizes.length === 0) {\n return image.original\n ? {\n width: image.originalSize[0],\n height: image.originalSize[1],\n image: image.original,\n }\n : null;\n }\n\n const sortedSizes = availableSizes\n .map((size) => {\n return {\n size,\n match: sizesMatchWanted(size[0], size[1], wantedWidth, wantedHeight),\n isLoaded: isLoaded(\n image.$jazz.raw.get(size[2]) as CoID<any> | undefined,\n ),\n };\n })\n .sort((a, b) => a.match - b.match);\n\n // We try to find the better already loaded image\n // note: `toReversed` is not available in react-native.\n const bestLoaded = [...sortedSizes]\n .reverse()\n .find((el) => el.isLoaded && image[el.size[2]]?.getChunks());\n\n // if I can't find a good match, let's use the highest resolution\n const bestTarget =\n sortedSizes.find((el) => el.match > 0.95) || sortedSizes.at(-1);\n\n // if the best target is already loaded, we are done\n if (image[bestTarget!.size[2]]?.getChunks()) {\n return image[bestTarget!.size[2]]\n ? {\n width: bestTarget!.size[0],\n height: bestTarget!.size[1],\n image: image[bestTarget!.size[2]]!,\n }\n : null;\n }\n\n // if the best already loaded is not the best target\n // let's trigger the load of the best target\n if (bestLoaded) {\n image[bestTarget!.size[2]]?.getChunks();\n return image[bestLoaded.size[2]]\n ? {\n width: bestLoaded.size[0],\n height: bestLoaded.size[1],\n image: image[bestLoaded.size[2]]!,\n }\n : null;\n }\n\n // if nothing is loaded, then start fetching all the images till the best\n for (let size of sortedSizes) {\n if (size.match <= bestTarget!.match) {\n image[size.size[2]]?.getChunks();\n }\n }\n\n return null;\n}\n\nfunction sizesMatchWanted(\n w: number,\n h: number,\n wantedW: number,\n wantedH: number,\n): number {\n const area1 = w * h;\n const area2 = wantedW * wantedH;\n\n const areaRatio = area1 / area2;\n\n // // Below 0.95 means the image is too small, we don't want to upscale it\n // if (areaRatio < 0.95) {\n // return 9999;\n // }\n\n return areaRatio;\n}\n\nfunction isLoaded(id: CoID<any> | null | undefined): boolean {\n if (!id) {\n return false;\n }\n\n return !!Account.getMe().$jazz.localNode.getLoaded(id);\n}\n\nexport async function loadImage(\n imageOrId: ImageDefinition | string,\n): Promise<{ width: number; height: number; image: FileStream } | null> {\n if (typeof imageOrId === \"string\") {\n const image = await ImageDefinition.load(imageOrId, {\n resolve: {\n original: true,\n },\n });\n\n if (image === null || image.original === null) {\n return null;\n }\n\n return {\n width: image.originalSize[0],\n height: image.originalSize[1],\n image: image.original,\n };\n }\n\n if (!imageOrId.original) {\n console.warn(\"Unable to find the original image\");\n return null;\n }\n\n const loadedOriginal = await FileStream.load(imageOrId.original.$jazz.id);\n\n if (!loadedOriginal) {\n console.warn(\"Unable to find the original image\");\n return null;\n }\n\n return {\n width: imageOrId.originalSize[0],\n height: imageOrId.originalSize[1],\n image: loadedOriginal,\n };\n}\n\nexport async function loadImageBySize(\n imageOrId: ImageDefinition | string,\n wantedWidth: number,\n wantedHeight: number,\n): Promise<{ width: number; height: number; image: FileStream } | null> {\n // @ts-expect-error The resolved type for CoMap does not include catchall properties\n const image: ImageDefinition | null =\n typeof imageOrId === \"string\"\n ? await ImageDefinition.load(imageOrId)\n : imageOrId;\n\n if (image === null) {\n return null;\n }\n\n if (image.progressive === false) {\n return loadImage(imageOrId);\n }\n\n const availableSizes: [number, number, string][] = image.$jazz.raw\n .keys()\n .filter((key) => /^\\d+x\\d+$/.test(key))\n .map((key) => {\n const [w, h] = key.split(\"x\").map(Number) as [number, number];\n return [w, h, key];\n });\n\n if (availableSizes.length === 0) {\n return null;\n }\n\n const sortedSizes = availableSizes\n .map((size) => ({\n size,\n match: sizesMatchWanted(size[0], size[1], wantedWidth, wantedHeight),\n }))\n .sort((a, b) => a.match - b.match);\n\n const bestTarget =\n sortedSizes.find((el) => el.match > 0.95) || sortedSizes.at(-1)!;\n\n // The image's `wxh` keys reference FileStream.\n // image[bestTarget.size[2]] returns undefined if FileStream hasn't loaded yet.\n // Since we only need the file's ID to fetch it later, we check the raw _refs\n // which contain only the linked covalue's ID.\n const file = image.$jazz.refs[bestTarget.size[2]];\n\n if (!file) {\n return null;\n }\n\n const loadedFile = await FileStream.load(file.id);\n\n if (!loadedFile) {\n return null;\n }\n\n return {\n width: bestTarget.size[0],\n height: bestTarget.size[1],\n image: loadedFile,\n };\n}\n","import {\n Account,\n FileStream,\n Group,\n ImageDefinition,\n type Loaded,\n} from \"jazz-tools\";\n\nexport type SourceType = Blob | File | string;\n\nexport type ResizeOutput = Blob | string;\n\nexport type CreateImageOptions = {\n /** The owner of the image. Can be either a Group or Account. If not specified, the current user will be the owner. */\n owner?: Group | Account;\n /**\n * Controls placeholder generation for the image.\n * - `\"blur\"`: Generates a blurred placeholder image (default)\n * - `false`: No placeholder is generated\n * @default \"blur\"\n */\n placeholder?: \"blur\" | false;\n /**\n * Maximum size constraint for the image. The image will be resized to fit within this size while maintaining aspect ratio.\n * If the image is smaller than maxSize in both dimensions, no resizing occurs.\n * @example 1024 // Resizes image to fit within 1024px in the largest dimension\n */\n maxSize?: number; // | [number, number];\n /**\n * The progressive loading pattern is a technique that allows images to load incrementally, starting with a small version and gradually replacing it with a larger version as it becomes available.\n * This is useful for improving the user experience by showing a placeholder while the image is loading.\n *\n * Passing progressive: true to createImage() will create internal smaller versions of the image for future uses.\n *\n * @default false\n */\n progressive?: boolean;\n};\n\nexport type CreateImageImpl<\n TSourceType = SourceType,\n TResizeOutput = ResizeOutput,\n> = {\n createFileStreamFromSource: (\n imageBlobOrFile: TSourceType | TResizeOutput,\n owner?: Group | Account,\n ) => Promise<FileStream>;\n getImageSize: (\n imageBlobOrFile: TSourceType,\n ) => Promise<{ width: number; height: number }>;\n getPlaceholderBase64: (imageBlobOrFile: TSourceType) => Promise<string>;\n resize: (\n imageBlobOrFile: TSourceType,\n width: number,\n height: number,\n ) => Promise<TResizeOutput>;\n};\n\nexport function createImageFactory<TSourceType, TResizeOutput>(\n impl: CreateImageImpl<TSourceType, TResizeOutput>,\n imageTypeGuard?: (imageBlobOrFile: TSourceType) => void,\n) {\n return (source: TSourceType, options?: CreateImageOptions) => {\n imageTypeGuard?.(source);\n return createImage(source, options ?? {}, impl);\n };\n}\n\nasync function createImage<TSourceType, TResizeOutput>(\n imageBlobOrFile: TSourceType,\n options: CreateImageOptions,\n impl: CreateImageImpl<TSourceType, TResizeOutput>,\n): Promise<Loaded<typeof ImageDefinition, { $each: true }>> {\n // Get the original size of the image\n const { width: originalWidth, height: originalHeight } =\n await impl.getImageSize(imageBlobOrFile);\n\n const def: {\n originalSize: [number, number];\n progressive: boolean;\n placeholderDataURL: string | undefined;\n original?: FileStream;\n files: Record<string, FileStream>;\n } = {\n originalSize: [originalWidth, originalHeight],\n progressive: false,\n placeholderDataURL: undefined,\n files: {},\n };\n\n // Placeholder\n if (options?.placeholder === \"blur\") {\n def.placeholderDataURL = await impl.getPlaceholderBase64(imageBlobOrFile);\n }\n\n /**\n * Original\n *\n * Save the original image.\n * If the maxSize is set, resize the image to the maxSize if needed\n */\n if (options?.maxSize === undefined) {\n def.original = await impl.createFileStreamFromSource(\n imageBlobOrFile,\n options?.owner,\n );\n def.files[`${originalWidth}x${originalHeight}`] = def.original;\n } else if (\n options?.maxSize >= originalWidth &&\n options?.maxSize >= originalHeight\n ) {\n // no resizes required, just return the original image\n def.original = await impl.createFileStreamFromSource(\n imageBlobOrFile,\n options?.owner,\n );\n def.files[`${originalWidth}x${originalHeight}`] = def.original;\n } else {\n const { width, height } = getNewDimensions(\n originalWidth,\n originalHeight,\n options.maxSize,\n );\n\n const blob = await impl.resize(imageBlobOrFile, width, height);\n def.originalSize = [width, height];\n def.original = await impl.createFileStreamFromSource(blob, options?.owner);\n def.files[`${width}x${height}`] = def.original;\n }\n\n const imageCoValue = ImageDefinition.create(\n {\n originalSize: def.originalSize,\n progressive: def.progressive,\n placeholderDataURL: def.placeholderDataURL,\n original: def.original,\n ...def.files,\n },\n options?.owner,\n );\n\n /**\n * Progressive loading\n *\n * Save a set of resized images using three sizes: 256, 1024, 2048\n *\n * On the client side, the image will be loaded progressively, starting from the smallest size and increasing the size until the original size is reached.\n */\n if (options?.progressive) {\n imageCoValue.$jazz.set(\"progressive\", true);\n\n const resizes = ([256, 1024, 2048] as const).filter(\n (s) =>\n s <\n Math.max(imageCoValue.originalSize[0], imageCoValue.originalSize[1]),\n );\n\n for (const size of resizes) {\n const { width, height } = getNewDimensions(\n originalWidth,\n originalHeight,\n size,\n );\n\n const blob = await impl.resize(imageBlobOrFile, width, height);\n imageCoValue.$jazz.set(\n `${width}x${height}`,\n await impl.createFileStreamFromSource(blob, options?.owner),\n );\n }\n }\n\n return imageCoValue;\n}\n\nconst getNewDimensions = (\n originalWidth: number,\n originalHeight: number,\n maxSize: number,\n) => {\n if (originalWidth > originalHeight) {\n return {\n width: maxSize,\n height: Math.round(maxSize * (originalHeight / originalWidth)),\n };\n }\n\n return {\n width: Math.round(maxSize * (originalWidth / originalHeight)),\n height: maxSize,\n };\n};\n"],"mappings":"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","names":["ImageDefinition"]}