zitejs 0.9.117 → 0.9.119

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.
@@ -96,6 +96,12 @@ const PREBUNDLED_LIBS = {
96
96
  '@elevenlabs/elevenlabs-js': '__elevenlabs__.js',
97
97
  'resend': '__resend__.js',
98
98
  '@clickhouse/client-web': '__clickhouse__.js',
99
+ // Three packages, one module: the presigners take an S3Client instance, so a
100
+ // separate bundle each would hand them a structurally identical but distinct
101
+ // class. The worker bundles all three together for that reason.
102
+ '@aws-sdk/client-s3': '__aws-s3__.js',
103
+ '@aws-sdk/s3-request-presigner': '__aws-s3__.js',
104
+ '@aws-sdk/s3-presigned-post': '__aws-s3__.js',
99
105
  };
100
106
  exports.BASE_BUILD_OPTIONS = {
101
107
  bundle: true,
@@ -2,6 +2,7 @@ export type UploadData = string | Blob | ArrayBuffer | File;
2
2
  export declare class FileUploadError extends Error {
3
3
  constructor(message: string);
4
4
  }
5
+ export declare function toUploadBlob(data: UploadData): Blob;
5
6
  export declare function uploadFile({ data, filename, }: {
6
7
  data: UploadData;
7
8
  filename: string;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FileUploadError = void 0;
4
+ exports.toUploadBlob = toUploadBlob;
4
5
  exports.uploadFile = uploadFile;
5
6
  exports.useUpload = useUpload;
6
7
  const react_1 = require("react");
@@ -24,33 +25,35 @@ function getZiteAppMode(hostname) {
24
25
  }
25
26
  return 'live';
26
27
  }
27
- function readBlobAsDataUrl(blob) {
28
- return new Promise((resolve, reject) => {
29
- const reader = new FileReader();
30
- reader.onload = () => resolve(reader.result);
31
- reader.onerror = () => reject(reader.error ?? new Error('Failed to read file'));
32
- reader.readAsDataURL(blob);
33
- });
28
+ function decodeDataUrl(dataUrl) {
29
+ const comma = dataUrl.indexOf(',');
30
+ if (comma === -1) {
31
+ throw new FileUploadError('Invalid data URL');
32
+ }
33
+ const header = dataUrl.slice(0, comma);
34
+ const payload = dataUrl.slice(comma + 1);
35
+ const mimeMatch = /^data:([^;,]*)/.exec(header);
36
+ const mimeType = mimeMatch?.[1] || 'application/octet-stream';
37
+ const bytes = Uint8Array.from(atob(payload), c => c.charCodeAt(0));
38
+ return new Blob([bytes], { type: mimeType });
34
39
  }
35
- async function toDataUrl(data) {
36
- if (data instanceof Blob) {
37
- return readBlobAsDataUrl(data);
40
+ function toUploadBlob(data) {
41
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
42
+ return data;
38
43
  }
39
44
  if (data instanceof ArrayBuffer) {
40
- const bytes = new Uint8Array(data);
41
- let binary = '';
42
- for (let i = 0; i < bytes.byteLength; i++) {
43
- binary += String.fromCharCode(bytes[i]);
44
- }
45
- return 'data:application/octet-stream;base64,' + btoa(binary);
45
+ return new Blob([data], { type: 'application/octet-stream' });
46
46
  }
47
47
  if (typeof data === 'string') {
48
48
  if (data.startsWith('data:'))
49
- return data;
50
- if (/^[A-Za-z0-9+/]+=*$/.test(data)) {
51
- return 'data:application/octet-stream;base64,' + data;
49
+ return decodeDataUrl(data);
50
+ // Only treat as raw base64 when the string is padded/aligned — otherwise
51
+ // short text like "hello" matches the alphabet and atob throws.
52
+ if (data.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(data)) {
53
+ const bytes = Uint8Array.from(atob(data), c => c.charCodeAt(0));
54
+ return new Blob([bytes], { type: 'application/octet-stream' });
52
55
  }
53
- return 'data:text/plain;base64,' + btoa(unescape(encodeURIComponent(data)));
56
+ return new Blob([data], { type: 'text/plain' });
54
57
  }
55
58
  throw new FileUploadError('Invalid data format. Expected string, Blob, ArrayBuffer, or File.');
56
59
  }
@@ -62,36 +65,55 @@ function resolveFilename(data, filename) {
62
65
  }
63
66
  throw new FileUploadError('A filename is required unless the uploaded data is a File.');
64
67
  }
65
- async function performUpload(data, filename) {
66
- const body = await toDataUrl(data);
67
- const mode = getZiteAppMode(window.location.hostname);
68
- const flowId = (0, config_js_1.getFlowId)();
68
+ async function readErrorMessage(res, fallback) {
69
+ try {
70
+ const err = (await res.json());
71
+ if (err.message)
72
+ return err.message;
73
+ }
74
+ catch { }
75
+ return fallback;
76
+ }
77
+ function authHeaders() {
69
78
  const token = typeof localStorage !== 'undefined'
70
79
  ? localStorage.getItem('zite.auth.token')
71
80
  : null;
72
- const res = await fetch((0, config_js_1.getApiUrl)() + '/v1/zite/public/' + flowId + '/upload?mode=' + mode, {
81
+ return token ? { Authorization: 'Bearer ' + token } : {};
82
+ }
83
+ async function performUpload(data, filename) {
84
+ const blob = toUploadBlob(data);
85
+ const mode = getZiteAppMode(window.location.hostname);
86
+ const flowId = (0, config_js_1.getFlowId)();
87
+ const sessionRes = await fetch((0, config_js_1.getApiUrl)() + '/v1/zite/public/' + flowId + '/upload-session?mode=' + mode, {
73
88
  method: 'POST',
74
89
  headers: {
75
90
  'Content-Type': 'application/json',
76
- ...(token ? { Authorization: 'Bearer ' + token } : {}),
91
+ ...authHeaders(),
77
92
  },
78
- body: JSON.stringify({ data: body, filename }),
93
+ body: JSON.stringify({
94
+ filename,
95
+ uploadLength: blob.size,
96
+ contentType: blob.type || 'application/octet-stream',
97
+ }),
79
98
  });
80
- if (!res.ok) {
81
- let message = 'Upload failed';
82
- try {
83
- const err = (await res.json());
84
- if (err.message)
85
- message = err.message;
86
- }
87
- catch { }
88
- throw new FileUploadError(message);
99
+ if (!sessionRes.ok) {
100
+ throw new FileUploadError(await readErrorMessage(sessionRes, 'Upload failed'));
89
101
  }
90
- const result = (await res.json());
91
- if (!result.success || !result.fileUrl) {
92
- throw new FileUploadError(result.message ?? 'Upload failed');
102
+ const session = (await sessionRes.json());
103
+ if (!session.success || !session.presignedUrl || !session.fileUrl) {
104
+ throw new FileUploadError(session.message ?? 'Upload failed');
105
+ }
106
+ const putRes = await fetch(session.presignedUrl, {
107
+ method: 'PUT',
108
+ headers: {
109
+ 'Content-Type': session.contentType || blob.type || 'application/octet-stream',
110
+ },
111
+ body: blob,
112
+ });
113
+ if (!putRes.ok) {
114
+ throw new FileUploadError('Upload failed');
93
115
  }
94
- return result.fileUrl;
116
+ return session.fileUrl;
95
117
  }
96
118
  async function uploadFile({ data, filename, }) {
97
119
  return { fileUrl: await performUpload(data, filename) };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_js_1 = require("./index.js");
5
+ vitest_1.vi.mock('../auth/config.js', () => ({
6
+ getApiUrl: () => 'https://api.example.com',
7
+ getFlowId: () => 'app_123',
8
+ }));
9
+ const fetchMock = vitest_1.vi.fn();
10
+ (0, vitest_1.beforeEach)(() => {
11
+ fetchMock.mockReset();
12
+ vitest_1.vi.stubGlobal('fetch', fetchMock);
13
+ vitest_1.vi.stubGlobal('window', { location: { hostname: 'portal.acme.com' } });
14
+ vitest_1.vi.stubGlobal('localStorage', { getItem: () => null });
15
+ });
16
+ (0, vitest_1.afterEach)(() => {
17
+ vitest_1.vi.unstubAllGlobals();
18
+ });
19
+ (0, vitest_1.describe)('toUploadBlob', () => {
20
+ (0, vitest_1.it)('wraps a string as text/plain and preserves File/Blob size', () => {
21
+ (0, vitest_1.expect)((0, index_js_1.toUploadBlob)('hello').size).toBe(5);
22
+ (0, vitest_1.expect)((0, index_js_1.toUploadBlob)('hello').type).toBe('text/plain');
23
+ const file = new File(['abc'], 'note.txt', { type: 'text/plain' });
24
+ (0, vitest_1.expect)((0, index_js_1.toUploadBlob)(file).size).toBe(3);
25
+ });
26
+ });
27
+ (0, vitest_1.describe)('uploadFile', () => {
28
+ (0, vitest_1.it)('requests a session then PUTs the bytes to the presigned URL', async () => {
29
+ const file = new File(['hello-world'], 'hello.txt', { type: 'text/plain' });
30
+ fetchMock
31
+ .mockResolvedValueOnce({
32
+ ok: true,
33
+ json: async () => ({
34
+ success: true,
35
+ presignedUrl: 'https://s3.example.com/put',
36
+ fileUrl: 'https://uploads.zite.com/orgid-1/zite-uploads/app_123/hello.txt',
37
+ contentType: 'text/plain',
38
+ }),
39
+ })
40
+ .mockResolvedValueOnce({ ok: true });
41
+ const result = await (0, index_js_1.uploadFile)({ data: file, filename: 'hello.txt' });
42
+ (0, vitest_1.expect)(result.fileUrl).toContain('hello.txt');
43
+ (0, vitest_1.expect)(fetchMock).toHaveBeenCalledTimes(2);
44
+ (0, vitest_1.expect)(fetchMock.mock.calls[0][0]).toBe('https://api.example.com/v1/zite/public/app_123/upload-session?mode=live');
45
+ const sessionInit = fetchMock.mock.calls[0][1];
46
+ (0, vitest_1.expect)(JSON.parse(sessionInit.body)).toEqual({
47
+ filename: 'hello.txt',
48
+ uploadLength: 11,
49
+ contentType: 'text/plain',
50
+ });
51
+ (0, vitest_1.expect)(fetchMock.mock.calls[1][0]).toBe('https://s3.example.com/put');
52
+ (0, vitest_1.expect)(fetchMock.mock.calls[1][1].method).toBe('PUT');
53
+ });
54
+ (0, vitest_1.it)('surfaces the plan-limit message from the session endpoint', async () => {
55
+ fetchMock.mockResolvedValueOnce({
56
+ ok: false,
57
+ json: async () => ({
58
+ message: 'File size exceeds maximum of 20 MB',
59
+ }),
60
+ });
61
+ const upload = (0, index_js_1.uploadFile)({
62
+ data: new File(['x'], 'big.bin'),
63
+ filename: 'big.bin',
64
+ });
65
+ await (0, vitest_1.expect)(upload).rejects.toBeInstanceOf(index_js_1.FileUploadError);
66
+ await (0, vitest_1.expect)(upload).rejects.toThrow('File size exceeds maximum of 20 MB');
67
+ });
68
+ });
@@ -59,6 +59,12 @@ const PREBUNDLED_LIBS = {
59
59
  '@elevenlabs/elevenlabs-js': '__elevenlabs__.js',
60
60
  'resend': '__resend__.js',
61
61
  '@clickhouse/client-web': '__clickhouse__.js',
62
+ // Three packages, one module: the presigners take an S3Client instance, so a
63
+ // separate bundle each would hand them a structurally identical but distinct
64
+ // class. The worker bundles all three together for that reason.
65
+ '@aws-sdk/client-s3': '__aws-s3__.js',
66
+ '@aws-sdk/s3-request-presigner': '__aws-s3__.js',
67
+ '@aws-sdk/s3-presigned-post': '__aws-s3__.js',
62
68
  };
63
69
  export const BASE_BUILD_OPTIONS = {
64
70
  bundle: true,
@@ -2,6 +2,7 @@ export type UploadData = string | Blob | ArrayBuffer | File;
2
2
  export declare class FileUploadError extends Error {
3
3
  constructor(message: string);
4
4
  }
5
+ export declare function toUploadBlob(data: UploadData): Blob;
5
6
  export declare function uploadFile({ data, filename, }: {
6
7
  data: UploadData;
7
8
  filename: string;
@@ -18,33 +18,35 @@ function getZiteAppMode(hostname) {
18
18
  }
19
19
  return 'live';
20
20
  }
21
- function readBlobAsDataUrl(blob) {
22
- return new Promise((resolve, reject) => {
23
- const reader = new FileReader();
24
- reader.onload = () => resolve(reader.result);
25
- reader.onerror = () => reject(reader.error ?? new Error('Failed to read file'));
26
- reader.readAsDataURL(blob);
27
- });
21
+ function decodeDataUrl(dataUrl) {
22
+ const comma = dataUrl.indexOf(',');
23
+ if (comma === -1) {
24
+ throw new FileUploadError('Invalid data URL');
25
+ }
26
+ const header = dataUrl.slice(0, comma);
27
+ const payload = dataUrl.slice(comma + 1);
28
+ const mimeMatch = /^data:([^;,]*)/.exec(header);
29
+ const mimeType = mimeMatch?.[1] || 'application/octet-stream';
30
+ const bytes = Uint8Array.from(atob(payload), c => c.charCodeAt(0));
31
+ return new Blob([bytes], { type: mimeType });
28
32
  }
29
- async function toDataUrl(data) {
30
- if (data instanceof Blob) {
31
- return readBlobAsDataUrl(data);
33
+ export function toUploadBlob(data) {
34
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
35
+ return data;
32
36
  }
33
37
  if (data instanceof ArrayBuffer) {
34
- const bytes = new Uint8Array(data);
35
- let binary = '';
36
- for (let i = 0; i < bytes.byteLength; i++) {
37
- binary += String.fromCharCode(bytes[i]);
38
- }
39
- return 'data:application/octet-stream;base64,' + btoa(binary);
38
+ return new Blob([data], { type: 'application/octet-stream' });
40
39
  }
41
40
  if (typeof data === 'string') {
42
41
  if (data.startsWith('data:'))
43
- return data;
44
- if (/^[A-Za-z0-9+/]+=*$/.test(data)) {
45
- return 'data:application/octet-stream;base64,' + data;
42
+ return decodeDataUrl(data);
43
+ // Only treat as raw base64 when the string is padded/aligned — otherwise
44
+ // short text like "hello" matches the alphabet and atob throws.
45
+ if (data.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(data)) {
46
+ const bytes = Uint8Array.from(atob(data), c => c.charCodeAt(0));
47
+ return new Blob([bytes], { type: 'application/octet-stream' });
46
48
  }
47
- return 'data:text/plain;base64,' + btoa(unescape(encodeURIComponent(data)));
49
+ return new Blob([data], { type: 'text/plain' });
48
50
  }
49
51
  throw new FileUploadError('Invalid data format. Expected string, Blob, ArrayBuffer, or File.');
50
52
  }
@@ -56,36 +58,55 @@ function resolveFilename(data, filename) {
56
58
  }
57
59
  throw new FileUploadError('A filename is required unless the uploaded data is a File.');
58
60
  }
59
- async function performUpload(data, filename) {
60
- const body = await toDataUrl(data);
61
- const mode = getZiteAppMode(window.location.hostname);
62
- const flowId = getFlowId();
61
+ async function readErrorMessage(res, fallback) {
62
+ try {
63
+ const err = (await res.json());
64
+ if (err.message)
65
+ return err.message;
66
+ }
67
+ catch { }
68
+ return fallback;
69
+ }
70
+ function authHeaders() {
63
71
  const token = typeof localStorage !== 'undefined'
64
72
  ? localStorage.getItem('zite.auth.token')
65
73
  : null;
66
- const res = await fetch(getApiUrl() + '/v1/zite/public/' + flowId + '/upload?mode=' + mode, {
74
+ return token ? { Authorization: 'Bearer ' + token } : {};
75
+ }
76
+ async function performUpload(data, filename) {
77
+ const blob = toUploadBlob(data);
78
+ const mode = getZiteAppMode(window.location.hostname);
79
+ const flowId = getFlowId();
80
+ const sessionRes = await fetch(getApiUrl() + '/v1/zite/public/' + flowId + '/upload-session?mode=' + mode, {
67
81
  method: 'POST',
68
82
  headers: {
69
83
  'Content-Type': 'application/json',
70
- ...(token ? { Authorization: 'Bearer ' + token } : {}),
84
+ ...authHeaders(),
71
85
  },
72
- body: JSON.stringify({ data: body, filename }),
86
+ body: JSON.stringify({
87
+ filename,
88
+ uploadLength: blob.size,
89
+ contentType: blob.type || 'application/octet-stream',
90
+ }),
73
91
  });
74
- if (!res.ok) {
75
- let message = 'Upload failed';
76
- try {
77
- const err = (await res.json());
78
- if (err.message)
79
- message = err.message;
80
- }
81
- catch { }
82
- throw new FileUploadError(message);
92
+ if (!sessionRes.ok) {
93
+ throw new FileUploadError(await readErrorMessage(sessionRes, 'Upload failed'));
83
94
  }
84
- const result = (await res.json());
85
- if (!result.success || !result.fileUrl) {
86
- throw new FileUploadError(result.message ?? 'Upload failed');
95
+ const session = (await sessionRes.json());
96
+ if (!session.success || !session.presignedUrl || !session.fileUrl) {
97
+ throw new FileUploadError(session.message ?? 'Upload failed');
98
+ }
99
+ const putRes = await fetch(session.presignedUrl, {
100
+ method: 'PUT',
101
+ headers: {
102
+ 'Content-Type': session.contentType || blob.type || 'application/octet-stream',
103
+ },
104
+ body: blob,
105
+ });
106
+ if (!putRes.ok) {
107
+ throw new FileUploadError('Upload failed');
87
108
  }
88
- return result.fileUrl;
109
+ return session.fileUrl;
89
110
  }
90
111
  export async function uploadFile({ data, filename, }) {
91
112
  return { fileUrl: await performUpload(data, filename) };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { FileUploadError, toUploadBlob, uploadFile } from './index.js';
3
+ vi.mock('../auth/config.js', () => ({
4
+ getApiUrl: () => 'https://api.example.com',
5
+ getFlowId: () => 'app_123',
6
+ }));
7
+ const fetchMock = vi.fn();
8
+ beforeEach(() => {
9
+ fetchMock.mockReset();
10
+ vi.stubGlobal('fetch', fetchMock);
11
+ vi.stubGlobal('window', { location: { hostname: 'portal.acme.com' } });
12
+ vi.stubGlobal('localStorage', { getItem: () => null });
13
+ });
14
+ afterEach(() => {
15
+ vi.unstubAllGlobals();
16
+ });
17
+ describe('toUploadBlob', () => {
18
+ it('wraps a string as text/plain and preserves File/Blob size', () => {
19
+ expect(toUploadBlob('hello').size).toBe(5);
20
+ expect(toUploadBlob('hello').type).toBe('text/plain');
21
+ const file = new File(['abc'], 'note.txt', { type: 'text/plain' });
22
+ expect(toUploadBlob(file).size).toBe(3);
23
+ });
24
+ });
25
+ describe('uploadFile', () => {
26
+ it('requests a session then PUTs the bytes to the presigned URL', async () => {
27
+ const file = new File(['hello-world'], 'hello.txt', { type: 'text/plain' });
28
+ fetchMock
29
+ .mockResolvedValueOnce({
30
+ ok: true,
31
+ json: async () => ({
32
+ success: true,
33
+ presignedUrl: 'https://s3.example.com/put',
34
+ fileUrl: 'https://uploads.zite.com/orgid-1/zite-uploads/app_123/hello.txt',
35
+ contentType: 'text/plain',
36
+ }),
37
+ })
38
+ .mockResolvedValueOnce({ ok: true });
39
+ const result = await uploadFile({ data: file, filename: 'hello.txt' });
40
+ expect(result.fileUrl).toContain('hello.txt');
41
+ expect(fetchMock).toHaveBeenCalledTimes(2);
42
+ expect(fetchMock.mock.calls[0][0]).toBe('https://api.example.com/v1/zite/public/app_123/upload-session?mode=live');
43
+ const sessionInit = fetchMock.mock.calls[0][1];
44
+ expect(JSON.parse(sessionInit.body)).toEqual({
45
+ filename: 'hello.txt',
46
+ uploadLength: 11,
47
+ contentType: 'text/plain',
48
+ });
49
+ expect(fetchMock.mock.calls[1][0]).toBe('https://s3.example.com/put');
50
+ expect(fetchMock.mock.calls[1][1].method).toBe('PUT');
51
+ });
52
+ it('surfaces the plan-limit message from the session endpoint', async () => {
53
+ fetchMock.mockResolvedValueOnce({
54
+ ok: false,
55
+ json: async () => ({
56
+ message: 'File size exceeds maximum of 20 MB',
57
+ }),
58
+ });
59
+ const upload = uploadFile({
60
+ data: new File(['x'], 'big.bin'),
61
+ filename: 'big.bin',
62
+ });
63
+ await expect(upload).rejects.toBeInstanceOf(FileUploadError);
64
+ await expect(upload).rejects.toThrow('File size exceeds maximum of 20 MB');
65
+ });
66
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.9.117",
4
- "description": "The Zite framework \u2014 build apps on Zite Database",
3
+ "version": "0.9.119",
4
+ "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
7
7
  "module": "./dist/esm/index.js",