@palettelab/sdk 0.1.14 → 0.1.15
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 +20 -2
- package/dist/index.d.mts +34 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.js +175 -5
- package/dist/index.mjs +175 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ Public frontend helpers exported by `@palettelab/sdk`:
|
|
|
166
166
|
- API: `apiFetch(path, init?)`, `apiUpload(path, file, fieldName?, extraFields?)`, `setBaseUrl(url)`, `getBaseUrl()`.
|
|
167
167
|
- Errors: `PaletteApiError`, `errorFromResponse(response)`, `isPaletteApiError(error)`.
|
|
168
168
|
- Data Rooms: `DataRoomClient`, `dataRooms`, plus `list`, `create`, `get`, `folder`, `ensureRoom`, `requireRoomByName`, `findRoomByName`, `createFolder`, `ensureFolder`, `findFolderByName`, `resolveFolderPath`, `findFileByName`, `requestUpload`, `confirmUpload`, and `uploadFile`.
|
|
169
|
-
- Storage: `StorageClient`, `uploadToSignedUrl(uploadUrl, file, contentType?)`.
|
|
169
|
+
- Storage: `StorageClient`, `upload(file, options)`, `resume(file, options)`, and `uploadToSignedUrl(uploadUrl, file, contentType?)`.
|
|
170
170
|
- Install config: `getInstallConfig(pluginId)`, `updateInstallConfig(pluginId, values)`.
|
|
171
171
|
- Organization/user: `UserClient`, `OrganizationClient`, including `current`, `updateProfile`, `listMine`, `listMembers`, `getMember`, `getMemberByEmail`, `inviteMember`, and `updateMemberRole`.
|
|
172
172
|
- Permissions: `hasPermission(ctx, permission)`, `hasAnyPermission(ctx, permissions)`, `hasAllPermissions(ctx, permissions)`.
|
|
@@ -286,12 +286,30 @@ Included clients:
|
|
|
286
286
|
- `palette.dataRooms.list()`, `create()`, `get()`, `folder()`, `createFolder()`, `ensureFolder()`, `findRoomByName()`, `findFolderByName()`, `resolveFolderPath()`, `findFileByName()`, and `uploadFile()`
|
|
287
287
|
- `palette.config.get()` and `palette.config.update(values)`, with optional plugin ID override
|
|
288
288
|
- `palette.permissions.has()`, `hasAny()`, and `hasAll()`
|
|
289
|
-
- `palette.storage.uploadToSignedUrl()`
|
|
289
|
+
- `palette.storage.upload(file, options)`, `resume(file, options)`, and `uploadToSignedUrl()`
|
|
290
290
|
- `palette.toast.success()`, `error()`, and `info()`
|
|
291
291
|
|
|
292
292
|
These helpers are intentionally thin wrappers over platform APIs. Apps can still
|
|
293
293
|
use `apiFetch()` directly for custom backend routes.
|
|
294
294
|
|
|
295
|
+
App storage uploads are scoped by Palette to:
|
|
296
|
+
|
|
297
|
+
```text
|
|
298
|
+
uploads/apps/{app_name}_{plugin_id}/{organisation_slug}_{organisation_id}/{file}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Declare `"storage"` in `platform_services`, then upload directly from the
|
|
302
|
+
browser with resumable chunking and progress:
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
const palette = createPaletteClient(usePlatform())
|
|
306
|
+
|
|
307
|
+
await palette.storage.upload(file, {
|
|
308
|
+
key: `receipts/${file.name}`,
|
|
309
|
+
onProgress: (p) => setPercent(p.percentage),
|
|
310
|
+
})
|
|
311
|
+
```
|
|
312
|
+
|
|
295
313
|
Member helpers operate on the active organisation. Use `members:read` for
|
|
296
314
|
listing or looking up members, and `members:write` for invitations or role
|
|
297
315
|
updates. When the runtime provides declared app permissions, these helpers check
|
package/dist/index.d.mts
CHANGED
|
@@ -107,8 +107,41 @@ declare class DataRoomClient {
|
|
|
107
107
|
}
|
|
108
108
|
declare const dataRooms: DataRoomClient;
|
|
109
109
|
|
|
110
|
+
type StorageUploadState = "starting" | "uploading" | "paused" | "complete";
|
|
111
|
+
type StorageUploadProgress = {
|
|
112
|
+
loaded: number;
|
|
113
|
+
total: number;
|
|
114
|
+
percentage: number;
|
|
115
|
+
chunkIndex: number;
|
|
116
|
+
chunkCount: number;
|
|
117
|
+
state: StorageUploadState;
|
|
118
|
+
};
|
|
119
|
+
type StorageUploadOptions = {
|
|
120
|
+
pluginId?: string;
|
|
121
|
+
key?: string;
|
|
122
|
+
contentType?: string;
|
|
123
|
+
chunkSize?: number;
|
|
124
|
+
resumable?: boolean;
|
|
125
|
+
onProgress?: (progress: StorageUploadProgress) => void;
|
|
126
|
+
signal?: AbortSignal;
|
|
127
|
+
};
|
|
128
|
+
type StorageUploadResult = {
|
|
129
|
+
uploadId: string;
|
|
130
|
+
mode: "gcs_resumable" | "local_resumable" | string;
|
|
131
|
+
bucket: string;
|
|
132
|
+
objectPath: string;
|
|
133
|
+
fileUrl: string;
|
|
134
|
+
contentType: string;
|
|
135
|
+
size: number;
|
|
136
|
+
};
|
|
110
137
|
declare function uploadToSignedUrl(uploadUrl: string, file: Blob, contentType?: string): Promise<void>;
|
|
111
138
|
declare class StorageClient {
|
|
139
|
+
private readonly ctx?;
|
|
140
|
+
constructor(ctx?: PlatformContext | undefined);
|
|
141
|
+
private pluginId;
|
|
142
|
+
private createSession;
|
|
143
|
+
upload(file: File, options?: StorageUploadOptions): Promise<StorageUploadResult>;
|
|
144
|
+
resume(file: File, options?: StorageUploadOptions): Promise<StorageUploadResult>;
|
|
112
145
|
uploadToSignedUrl: typeof uploadToSignedUrl;
|
|
113
146
|
}
|
|
114
147
|
|
|
@@ -179,4 +212,4 @@ declare function hasPermission(ctx: PlatformContext, permission: string): boolea
|
|
|
179
212
|
declare function hasAnyPermission(ctx: PlatformContext, permissions: string[]): boolean;
|
|
180
213
|
declare function hasAllPermissions(ctx: PlatformContext, permissions: string[]): boolean;
|
|
181
214
|
|
|
182
|
-
export { DataRoom, DataRoomClient, type DataRoomContents, DataRoomFile, DataRoomFolder, type DataRoomUploadOptions, type InstallConfig, type OrgInviteMemberInput, type OrgInviteMemberResponse, type OrgMember, type OrgMemberRole, OrgSummary, OrganizationClient, PaletteApiError, type PaletteClient, PlatformContext, type SandboxBridge, StorageClient, User, UserClient, apiFetch, apiUpload, createMockPlatformContext, createPaletteClient, createSandboxBridge, dataRooms, errorFromResponse, getBaseUrl, getInstallConfig, hasAllPermissions, hasAnyPermission, hasPermission, isPaletteApiError, isSandboxRuntime, setBaseUrl, updateInstallConfig, uploadToSignedUrl, withPluginProvider };
|
|
215
|
+
export { DataRoom, DataRoomClient, type DataRoomContents, DataRoomFile, DataRoomFolder, type DataRoomUploadOptions, type InstallConfig, type OrgInviteMemberInput, type OrgInviteMemberResponse, type OrgMember, type OrgMemberRole, OrgSummary, OrganizationClient, PaletteApiError, type PaletteClient, PlatformContext, type SandboxBridge, StorageClient, type StorageUploadOptions, type StorageUploadProgress, type StorageUploadResult, type StorageUploadState, User, UserClient, apiFetch, apiUpload, createMockPlatformContext, createPaletteClient, createSandboxBridge, dataRooms, errorFromResponse, getBaseUrl, getInstallConfig, hasAllPermissions, hasAnyPermission, hasPermission, isPaletteApiError, isSandboxRuntime, setBaseUrl, updateInstallConfig, uploadToSignedUrl, withPluginProvider };
|
package/dist/index.d.ts
CHANGED
|
@@ -107,8 +107,41 @@ declare class DataRoomClient {
|
|
|
107
107
|
}
|
|
108
108
|
declare const dataRooms: DataRoomClient;
|
|
109
109
|
|
|
110
|
+
type StorageUploadState = "starting" | "uploading" | "paused" | "complete";
|
|
111
|
+
type StorageUploadProgress = {
|
|
112
|
+
loaded: number;
|
|
113
|
+
total: number;
|
|
114
|
+
percentage: number;
|
|
115
|
+
chunkIndex: number;
|
|
116
|
+
chunkCount: number;
|
|
117
|
+
state: StorageUploadState;
|
|
118
|
+
};
|
|
119
|
+
type StorageUploadOptions = {
|
|
120
|
+
pluginId?: string;
|
|
121
|
+
key?: string;
|
|
122
|
+
contentType?: string;
|
|
123
|
+
chunkSize?: number;
|
|
124
|
+
resumable?: boolean;
|
|
125
|
+
onProgress?: (progress: StorageUploadProgress) => void;
|
|
126
|
+
signal?: AbortSignal;
|
|
127
|
+
};
|
|
128
|
+
type StorageUploadResult = {
|
|
129
|
+
uploadId: string;
|
|
130
|
+
mode: "gcs_resumable" | "local_resumable" | string;
|
|
131
|
+
bucket: string;
|
|
132
|
+
objectPath: string;
|
|
133
|
+
fileUrl: string;
|
|
134
|
+
contentType: string;
|
|
135
|
+
size: number;
|
|
136
|
+
};
|
|
110
137
|
declare function uploadToSignedUrl(uploadUrl: string, file: Blob, contentType?: string): Promise<void>;
|
|
111
138
|
declare class StorageClient {
|
|
139
|
+
private readonly ctx?;
|
|
140
|
+
constructor(ctx?: PlatformContext | undefined);
|
|
141
|
+
private pluginId;
|
|
142
|
+
private createSession;
|
|
143
|
+
upload(file: File, options?: StorageUploadOptions): Promise<StorageUploadResult>;
|
|
144
|
+
resume(file: File, options?: StorageUploadOptions): Promise<StorageUploadResult>;
|
|
112
145
|
uploadToSignedUrl: typeof uploadToSignedUrl;
|
|
113
146
|
}
|
|
114
147
|
|
|
@@ -179,4 +212,4 @@ declare function hasPermission(ctx: PlatformContext, permission: string): boolea
|
|
|
179
212
|
declare function hasAnyPermission(ctx: PlatformContext, permissions: string[]): boolean;
|
|
180
213
|
declare function hasAllPermissions(ctx: PlatformContext, permissions: string[]): boolean;
|
|
181
214
|
|
|
182
|
-
export { DataRoom, DataRoomClient, type DataRoomContents, DataRoomFile, DataRoomFolder, type DataRoomUploadOptions, type InstallConfig, type OrgInviteMemberInput, type OrgInviteMemberResponse, type OrgMember, type OrgMemberRole, OrgSummary, OrganizationClient, PaletteApiError, type PaletteClient, PlatformContext, type SandboxBridge, StorageClient, User, UserClient, apiFetch, apiUpload, createMockPlatformContext, createPaletteClient, createSandboxBridge, dataRooms, errorFromResponse, getBaseUrl, getInstallConfig, hasAllPermissions, hasAnyPermission, hasPermission, isPaletteApiError, isSandboxRuntime, setBaseUrl, updateInstallConfig, uploadToSignedUrl, withPluginProvider };
|
|
215
|
+
export { DataRoom, DataRoomClient, type DataRoomContents, DataRoomFile, DataRoomFolder, type DataRoomUploadOptions, type InstallConfig, type OrgInviteMemberInput, type OrgInviteMemberResponse, type OrgMember, type OrgMemberRole, OrgSummary, OrganizationClient, PaletteApiError, type PaletteClient, PlatformContext, type SandboxBridge, StorageClient, type StorageUploadOptions, type StorageUploadProgress, type StorageUploadResult, type StorageUploadState, User, UserClient, apiFetch, apiUpload, createMockPlatformContext, createPaletteClient, createSandboxBridge, dataRooms, errorFromResponse, getBaseUrl, getInstallConfig, hasAllPermissions, hasAnyPermission, hasPermission, isPaletteApiError, isSandboxRuntime, setBaseUrl, updateInstallConfig, uploadToSignedUrl, withPluginProvider };
|
package/dist/index.js
CHANGED
|
@@ -359,6 +359,8 @@ function hasAllPermissions(ctx, permissions) {
|
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
// src/storage.ts
|
|
362
|
+
var DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024;
|
|
363
|
+
var MIN_CHUNK_SIZE = 256 * 1024;
|
|
362
364
|
async function uploadToSignedUrl(uploadUrl, file, contentType = "application/octet-stream") {
|
|
363
365
|
const res = await fetch(uploadUrl, {
|
|
364
366
|
method: "PUT",
|
|
@@ -367,10 +369,178 @@ async function uploadToSignedUrl(uploadUrl, file, contentType = "application/oct
|
|
|
367
369
|
});
|
|
368
370
|
if (!res.ok) throw new Error(`Upload failed: ${res.statusText}`);
|
|
369
371
|
}
|
|
372
|
+
function normalizeChunkSize(value) {
|
|
373
|
+
const raw = Math.max(value || DEFAULT_CHUNK_SIZE, MIN_CHUNK_SIZE);
|
|
374
|
+
return Math.ceil(raw / MIN_CHUNK_SIZE) * MIN_CHUNK_SIZE;
|
|
375
|
+
}
|
|
376
|
+
function defaultPluginId() {
|
|
377
|
+
if (typeof window === "undefined") return "";
|
|
378
|
+
const match = window.location.pathname.match(/\/apps\/([^/?#]+)/);
|
|
379
|
+
return match ? decodeURIComponent(match[1]) : "";
|
|
380
|
+
}
|
|
381
|
+
function fileFingerprint(pluginId, file, key) {
|
|
382
|
+
return [pluginId, key || file.name, file.size, file.lastModified].join(":");
|
|
383
|
+
}
|
|
384
|
+
function sessionStorageKey(pluginId, fingerprint) {
|
|
385
|
+
return `palette:storage-upload:${pluginId}:${fingerprint}`;
|
|
386
|
+
}
|
|
387
|
+
function readStoredSession(pluginId, fingerprint) {
|
|
388
|
+
if (typeof window === "undefined") return null;
|
|
389
|
+
try {
|
|
390
|
+
const raw = window.localStorage.getItem(sessionStorageKey(pluginId, fingerprint));
|
|
391
|
+
if (!raw) return null;
|
|
392
|
+
const session = JSON.parse(raw);
|
|
393
|
+
return session.plugin_id === pluginId && session.fingerprint === fingerprint ? session : null;
|
|
394
|
+
} catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function writeStoredSession(pluginId, fingerprint, session) {
|
|
399
|
+
if (typeof window === "undefined") return;
|
|
400
|
+
const stored = { ...session, plugin_id: pluginId, fingerprint, updated_at: Date.now() };
|
|
401
|
+
window.localStorage.setItem(sessionStorageKey(pluginId, fingerprint), JSON.stringify(stored));
|
|
402
|
+
}
|
|
403
|
+
function clearStoredSession(pluginId, fingerprint) {
|
|
404
|
+
if (typeof window === "undefined") return;
|
|
405
|
+
window.localStorage.removeItem(sessionStorageKey(pluginId, fingerprint));
|
|
406
|
+
}
|
|
407
|
+
function absoluteApiUrl(pathOrUrl) {
|
|
408
|
+
return /^https?:\/\//i.test(pathOrUrl) ? pathOrUrl : `${getBaseUrl()}${pathOrUrl}`;
|
|
409
|
+
}
|
|
410
|
+
function isPaletteApiUpload(url) {
|
|
411
|
+
return url.startsWith(getBaseUrl()) || url.startsWith("/");
|
|
412
|
+
}
|
|
413
|
+
function report(loaded, total, chunkIndex, chunkCount, state, onProgress) {
|
|
414
|
+
onProgress?.({
|
|
415
|
+
loaded,
|
|
416
|
+
total,
|
|
417
|
+
percentage: total > 0 ? Math.min(100, Math.round(loaded / total * 1e4) / 100) : 100,
|
|
418
|
+
chunkIndex,
|
|
419
|
+
chunkCount,
|
|
420
|
+
state
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
function xhrPut(url, body, headers, signal, onUploadProgress) {
|
|
424
|
+
return new Promise((resolve, reject) => {
|
|
425
|
+
const xhr = new XMLHttpRequest();
|
|
426
|
+
xhr.open("PUT", absoluteApiUrl(url), true);
|
|
427
|
+
xhr.withCredentials = isPaletteApiUpload(url);
|
|
428
|
+
for (const [key, value] of Object.entries(headers)) xhr.setRequestHeader(key, value);
|
|
429
|
+
xhr.upload.onprogress = (event) => {
|
|
430
|
+
if (event.lengthComputable) onUploadProgress?.(event.loaded);
|
|
431
|
+
};
|
|
432
|
+
xhr.onload = () => {
|
|
433
|
+
const ok = [200, 201, 204, 308].includes(xhr.status);
|
|
434
|
+
if (!ok) reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
|
|
435
|
+
else resolve({ status: xhr.status, range: xhr.getResponseHeader("Range") });
|
|
436
|
+
};
|
|
437
|
+
xhr.onerror = () => reject(new Error("Upload failed"));
|
|
438
|
+
xhr.onabort = () => reject(new DOMException("Upload aborted", "AbortError"));
|
|
439
|
+
if (signal) {
|
|
440
|
+
if (signal.aborted) {
|
|
441
|
+
xhr.abort();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
signal.addEventListener("abort", () => xhr.abort(), { once: true });
|
|
445
|
+
}
|
|
446
|
+
xhr.send(body);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
function uploadedFromRange(range) {
|
|
450
|
+
if (!range) return 0;
|
|
451
|
+
const match = range.match(/bytes=0-(\d+)/);
|
|
452
|
+
return match ? Number(match[1]) + 1 : 0;
|
|
453
|
+
}
|
|
454
|
+
async function queryGcsOffset(session) {
|
|
455
|
+
try {
|
|
456
|
+
const result = await xhrPut(session.upload_url, new Blob([]), {
|
|
457
|
+
"Content-Range": `bytes */${session.size}`
|
|
458
|
+
});
|
|
459
|
+
if ([200, 201, 204].includes(result.status)) return session.size;
|
|
460
|
+
return uploadedFromRange(result.range);
|
|
461
|
+
} catch {
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async function queryLocalOffset(session) {
|
|
466
|
+
const res = await fetch(absoluteApiUrl(session.status_url), { credentials: "include" });
|
|
467
|
+
if (!res.ok) return 0;
|
|
468
|
+
const body = await res.json();
|
|
469
|
+
return Number(body.uploaded_bytes || 0);
|
|
470
|
+
}
|
|
471
|
+
async function queryOffset(session) {
|
|
472
|
+
if (session.mode === "local_resumable") return queryLocalOffset(session);
|
|
473
|
+
return queryGcsOffset(session);
|
|
474
|
+
}
|
|
370
475
|
var StorageClient = class {
|
|
371
|
-
constructor() {
|
|
476
|
+
constructor(ctx) {
|
|
477
|
+
this.ctx = ctx;
|
|
372
478
|
this.uploadToSignedUrl = uploadToSignedUrl;
|
|
373
479
|
}
|
|
480
|
+
pluginId(explicit) {
|
|
481
|
+
const resolved = explicit || this.ctx?.pluginId || defaultPluginId();
|
|
482
|
+
if (!resolved) throw new Error("pluginId is required for app storage uploads");
|
|
483
|
+
return resolved;
|
|
484
|
+
}
|
|
485
|
+
async createSession(pluginId, file, options) {
|
|
486
|
+
const res = await (this.ctx?.apiFetch || apiFetch)(`/api/v1/app-storage/${encodeURIComponent(pluginId)}/uploads`, {
|
|
487
|
+
method: "POST",
|
|
488
|
+
body: JSON.stringify({
|
|
489
|
+
filename: file.name,
|
|
490
|
+
content_type: options.contentType || file.type || "application/octet-stream",
|
|
491
|
+
size: file.size,
|
|
492
|
+
key: options.key || null,
|
|
493
|
+
resumable: options.resumable ?? true,
|
|
494
|
+
chunk_size: normalizeChunkSize(options.chunkSize)
|
|
495
|
+
})
|
|
496
|
+
});
|
|
497
|
+
return res.json();
|
|
498
|
+
}
|
|
499
|
+
async upload(file, options = {}) {
|
|
500
|
+
const pluginId = this.pluginId(options.pluginId);
|
|
501
|
+
const fingerprint = fileFingerprint(pluginId, file, options.key);
|
|
502
|
+
const stored = options.resumable === false ? null : readStoredSession(pluginId, fingerprint);
|
|
503
|
+
const session = stored || await this.createSession(pluginId, file, options);
|
|
504
|
+
writeStoredSession(pluginId, fingerprint, session);
|
|
505
|
+
const chunkSize = normalizeChunkSize(options.chunkSize || session.chunk_size);
|
|
506
|
+
const chunkCount = Math.max(1, Math.ceil(file.size / chunkSize));
|
|
507
|
+
let offset = stored ? await queryOffset(session) : 0;
|
|
508
|
+
report(offset, file.size, Math.floor(offset / chunkSize), chunkCount, "starting", options.onProgress);
|
|
509
|
+
while (offset < file.size) {
|
|
510
|
+
if (options.signal?.aborted) throw new DOMException("Upload aborted", "AbortError");
|
|
511
|
+
const start = offset;
|
|
512
|
+
const end = Math.min(file.size, start + chunkSize) - 1;
|
|
513
|
+
const chunk = file.slice(start, end + 1);
|
|
514
|
+
const chunkIndex = Math.floor(start / chunkSize) + 1;
|
|
515
|
+
const lastReportedBase = start;
|
|
516
|
+
await xhrPut(
|
|
517
|
+
session.upload_url,
|
|
518
|
+
chunk,
|
|
519
|
+
{
|
|
520
|
+
"Content-Type": session.content_type,
|
|
521
|
+
"Content-Range": `bytes ${start}-${end}/${file.size}`
|
|
522
|
+
},
|
|
523
|
+
options.signal,
|
|
524
|
+
(loaded) => report(lastReportedBase + loaded, file.size, chunkIndex, chunkCount, "uploading", options.onProgress)
|
|
525
|
+
);
|
|
526
|
+
offset = end + 1;
|
|
527
|
+
report(offset, file.size, chunkIndex, chunkCount, offset >= file.size ? "complete" : "uploading", options.onProgress);
|
|
528
|
+
writeStoredSession(pluginId, fingerprint, session);
|
|
529
|
+
}
|
|
530
|
+
clearStoredSession(pluginId, fingerprint);
|
|
531
|
+
return {
|
|
532
|
+
uploadId: session.upload_id,
|
|
533
|
+
mode: session.mode,
|
|
534
|
+
bucket: session.bucket,
|
|
535
|
+
objectPath: session.object_path,
|
|
536
|
+
fileUrl: session.file_url,
|
|
537
|
+
contentType: session.content_type,
|
|
538
|
+
size: session.size
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
async resume(file, options = {}) {
|
|
542
|
+
return this.upload(file, { ...options, resumable: true });
|
|
543
|
+
}
|
|
374
544
|
};
|
|
375
545
|
|
|
376
546
|
// src/user-org.ts
|
|
@@ -448,7 +618,7 @@ var OrganizationClient = class {
|
|
|
448
618
|
|
|
449
619
|
// src/palette-client.ts
|
|
450
620
|
function createPaletteClient(ctx) {
|
|
451
|
-
const
|
|
621
|
+
const defaultPluginId2 = () => {
|
|
452
622
|
if (ctx?.pluginId) return ctx.pluginId;
|
|
453
623
|
if (typeof window === "undefined") return "";
|
|
454
624
|
const match = window.location.pathname.match(/\/apps\/([^/?#]+)/);
|
|
@@ -458,15 +628,15 @@ function createPaletteClient(ctx) {
|
|
|
458
628
|
user: new UserClient(ctx),
|
|
459
629
|
organization: new OrganizationClient(ctx),
|
|
460
630
|
dataRooms: new DataRoomClient(),
|
|
461
|
-
storage: new StorageClient(),
|
|
631
|
+
storage: new StorageClient(ctx),
|
|
462
632
|
config: {
|
|
463
633
|
get: (pluginId = ctx?.pluginId ?? "") => {
|
|
464
|
-
const resolved = pluginId ||
|
|
634
|
+
const resolved = pluginId || defaultPluginId2();
|
|
465
635
|
if (!resolved) throw new Error("pluginId is required to read install config");
|
|
466
636
|
return getInstallConfig(resolved);
|
|
467
637
|
},
|
|
468
638
|
update: (values, pluginId = ctx?.pluginId ?? "") => {
|
|
469
|
-
const resolved = pluginId ||
|
|
639
|
+
const resolved = pluginId || defaultPluginId2();
|
|
470
640
|
if (!resolved) throw new Error("pluginId is required to update install config");
|
|
471
641
|
return updateInstallConfig(resolved, values);
|
|
472
642
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -302,6 +302,8 @@ function hasAllPermissions(ctx, permissions) {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
// src/storage.ts
|
|
305
|
+
var DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024;
|
|
306
|
+
var MIN_CHUNK_SIZE = 256 * 1024;
|
|
305
307
|
async function uploadToSignedUrl(uploadUrl, file, contentType = "application/octet-stream") {
|
|
306
308
|
const res = await fetch(uploadUrl, {
|
|
307
309
|
method: "PUT",
|
|
@@ -310,10 +312,178 @@ async function uploadToSignedUrl(uploadUrl, file, contentType = "application/oct
|
|
|
310
312
|
});
|
|
311
313
|
if (!res.ok) throw new Error(`Upload failed: ${res.statusText}`);
|
|
312
314
|
}
|
|
315
|
+
function normalizeChunkSize(value) {
|
|
316
|
+
const raw = Math.max(value || DEFAULT_CHUNK_SIZE, MIN_CHUNK_SIZE);
|
|
317
|
+
return Math.ceil(raw / MIN_CHUNK_SIZE) * MIN_CHUNK_SIZE;
|
|
318
|
+
}
|
|
319
|
+
function defaultPluginId() {
|
|
320
|
+
if (typeof window === "undefined") return "";
|
|
321
|
+
const match = window.location.pathname.match(/\/apps\/([^/?#]+)/);
|
|
322
|
+
return match ? decodeURIComponent(match[1]) : "";
|
|
323
|
+
}
|
|
324
|
+
function fileFingerprint(pluginId, file, key) {
|
|
325
|
+
return [pluginId, key || file.name, file.size, file.lastModified].join(":");
|
|
326
|
+
}
|
|
327
|
+
function sessionStorageKey(pluginId, fingerprint) {
|
|
328
|
+
return `palette:storage-upload:${pluginId}:${fingerprint}`;
|
|
329
|
+
}
|
|
330
|
+
function readStoredSession(pluginId, fingerprint) {
|
|
331
|
+
if (typeof window === "undefined") return null;
|
|
332
|
+
try {
|
|
333
|
+
const raw = window.localStorage.getItem(sessionStorageKey(pluginId, fingerprint));
|
|
334
|
+
if (!raw) return null;
|
|
335
|
+
const session = JSON.parse(raw);
|
|
336
|
+
return session.plugin_id === pluginId && session.fingerprint === fingerprint ? session : null;
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function writeStoredSession(pluginId, fingerprint, session) {
|
|
342
|
+
if (typeof window === "undefined") return;
|
|
343
|
+
const stored = { ...session, plugin_id: pluginId, fingerprint, updated_at: Date.now() };
|
|
344
|
+
window.localStorage.setItem(sessionStorageKey(pluginId, fingerprint), JSON.stringify(stored));
|
|
345
|
+
}
|
|
346
|
+
function clearStoredSession(pluginId, fingerprint) {
|
|
347
|
+
if (typeof window === "undefined") return;
|
|
348
|
+
window.localStorage.removeItem(sessionStorageKey(pluginId, fingerprint));
|
|
349
|
+
}
|
|
350
|
+
function absoluteApiUrl(pathOrUrl) {
|
|
351
|
+
return /^https?:\/\//i.test(pathOrUrl) ? pathOrUrl : `${getBaseUrl()}${pathOrUrl}`;
|
|
352
|
+
}
|
|
353
|
+
function isPaletteApiUpload(url) {
|
|
354
|
+
return url.startsWith(getBaseUrl()) || url.startsWith("/");
|
|
355
|
+
}
|
|
356
|
+
function report(loaded, total, chunkIndex, chunkCount, state, onProgress) {
|
|
357
|
+
onProgress?.({
|
|
358
|
+
loaded,
|
|
359
|
+
total,
|
|
360
|
+
percentage: total > 0 ? Math.min(100, Math.round(loaded / total * 1e4) / 100) : 100,
|
|
361
|
+
chunkIndex,
|
|
362
|
+
chunkCount,
|
|
363
|
+
state
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
function xhrPut(url, body, headers, signal, onUploadProgress) {
|
|
367
|
+
return new Promise((resolve, reject) => {
|
|
368
|
+
const xhr = new XMLHttpRequest();
|
|
369
|
+
xhr.open("PUT", absoluteApiUrl(url), true);
|
|
370
|
+
xhr.withCredentials = isPaletteApiUpload(url);
|
|
371
|
+
for (const [key, value] of Object.entries(headers)) xhr.setRequestHeader(key, value);
|
|
372
|
+
xhr.upload.onprogress = (event) => {
|
|
373
|
+
if (event.lengthComputable) onUploadProgress?.(event.loaded);
|
|
374
|
+
};
|
|
375
|
+
xhr.onload = () => {
|
|
376
|
+
const ok = [200, 201, 204, 308].includes(xhr.status);
|
|
377
|
+
if (!ok) reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
|
|
378
|
+
else resolve({ status: xhr.status, range: xhr.getResponseHeader("Range") });
|
|
379
|
+
};
|
|
380
|
+
xhr.onerror = () => reject(new Error("Upload failed"));
|
|
381
|
+
xhr.onabort = () => reject(new DOMException("Upload aborted", "AbortError"));
|
|
382
|
+
if (signal) {
|
|
383
|
+
if (signal.aborted) {
|
|
384
|
+
xhr.abort();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
signal.addEventListener("abort", () => xhr.abort(), { once: true });
|
|
388
|
+
}
|
|
389
|
+
xhr.send(body);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function uploadedFromRange(range) {
|
|
393
|
+
if (!range) return 0;
|
|
394
|
+
const match = range.match(/bytes=0-(\d+)/);
|
|
395
|
+
return match ? Number(match[1]) + 1 : 0;
|
|
396
|
+
}
|
|
397
|
+
async function queryGcsOffset(session) {
|
|
398
|
+
try {
|
|
399
|
+
const result = await xhrPut(session.upload_url, new Blob([]), {
|
|
400
|
+
"Content-Range": `bytes */${session.size}`
|
|
401
|
+
});
|
|
402
|
+
if ([200, 201, 204].includes(result.status)) return session.size;
|
|
403
|
+
return uploadedFromRange(result.range);
|
|
404
|
+
} catch {
|
|
405
|
+
return 0;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async function queryLocalOffset(session) {
|
|
409
|
+
const res = await fetch(absoluteApiUrl(session.status_url), { credentials: "include" });
|
|
410
|
+
if (!res.ok) return 0;
|
|
411
|
+
const body = await res.json();
|
|
412
|
+
return Number(body.uploaded_bytes || 0);
|
|
413
|
+
}
|
|
414
|
+
async function queryOffset(session) {
|
|
415
|
+
if (session.mode === "local_resumable") return queryLocalOffset(session);
|
|
416
|
+
return queryGcsOffset(session);
|
|
417
|
+
}
|
|
313
418
|
var StorageClient = class {
|
|
314
|
-
constructor() {
|
|
419
|
+
constructor(ctx) {
|
|
420
|
+
this.ctx = ctx;
|
|
315
421
|
this.uploadToSignedUrl = uploadToSignedUrl;
|
|
316
422
|
}
|
|
423
|
+
pluginId(explicit) {
|
|
424
|
+
const resolved = explicit || this.ctx?.pluginId || defaultPluginId();
|
|
425
|
+
if (!resolved) throw new Error("pluginId is required for app storage uploads");
|
|
426
|
+
return resolved;
|
|
427
|
+
}
|
|
428
|
+
async createSession(pluginId, file, options) {
|
|
429
|
+
const res = await (this.ctx?.apiFetch || apiFetch)(`/api/v1/app-storage/${encodeURIComponent(pluginId)}/uploads`, {
|
|
430
|
+
method: "POST",
|
|
431
|
+
body: JSON.stringify({
|
|
432
|
+
filename: file.name,
|
|
433
|
+
content_type: options.contentType || file.type || "application/octet-stream",
|
|
434
|
+
size: file.size,
|
|
435
|
+
key: options.key || null,
|
|
436
|
+
resumable: options.resumable ?? true,
|
|
437
|
+
chunk_size: normalizeChunkSize(options.chunkSize)
|
|
438
|
+
})
|
|
439
|
+
});
|
|
440
|
+
return res.json();
|
|
441
|
+
}
|
|
442
|
+
async upload(file, options = {}) {
|
|
443
|
+
const pluginId = this.pluginId(options.pluginId);
|
|
444
|
+
const fingerprint = fileFingerprint(pluginId, file, options.key);
|
|
445
|
+
const stored = options.resumable === false ? null : readStoredSession(pluginId, fingerprint);
|
|
446
|
+
const session = stored || await this.createSession(pluginId, file, options);
|
|
447
|
+
writeStoredSession(pluginId, fingerprint, session);
|
|
448
|
+
const chunkSize = normalizeChunkSize(options.chunkSize || session.chunk_size);
|
|
449
|
+
const chunkCount = Math.max(1, Math.ceil(file.size / chunkSize));
|
|
450
|
+
let offset = stored ? await queryOffset(session) : 0;
|
|
451
|
+
report(offset, file.size, Math.floor(offset / chunkSize), chunkCount, "starting", options.onProgress);
|
|
452
|
+
while (offset < file.size) {
|
|
453
|
+
if (options.signal?.aborted) throw new DOMException("Upload aborted", "AbortError");
|
|
454
|
+
const start = offset;
|
|
455
|
+
const end = Math.min(file.size, start + chunkSize) - 1;
|
|
456
|
+
const chunk = file.slice(start, end + 1);
|
|
457
|
+
const chunkIndex = Math.floor(start / chunkSize) + 1;
|
|
458
|
+
const lastReportedBase = start;
|
|
459
|
+
await xhrPut(
|
|
460
|
+
session.upload_url,
|
|
461
|
+
chunk,
|
|
462
|
+
{
|
|
463
|
+
"Content-Type": session.content_type,
|
|
464
|
+
"Content-Range": `bytes ${start}-${end}/${file.size}`
|
|
465
|
+
},
|
|
466
|
+
options.signal,
|
|
467
|
+
(loaded) => report(lastReportedBase + loaded, file.size, chunkIndex, chunkCount, "uploading", options.onProgress)
|
|
468
|
+
);
|
|
469
|
+
offset = end + 1;
|
|
470
|
+
report(offset, file.size, chunkIndex, chunkCount, offset >= file.size ? "complete" : "uploading", options.onProgress);
|
|
471
|
+
writeStoredSession(pluginId, fingerprint, session);
|
|
472
|
+
}
|
|
473
|
+
clearStoredSession(pluginId, fingerprint);
|
|
474
|
+
return {
|
|
475
|
+
uploadId: session.upload_id,
|
|
476
|
+
mode: session.mode,
|
|
477
|
+
bucket: session.bucket,
|
|
478
|
+
objectPath: session.object_path,
|
|
479
|
+
fileUrl: session.file_url,
|
|
480
|
+
contentType: session.content_type,
|
|
481
|
+
size: session.size
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
async resume(file, options = {}) {
|
|
485
|
+
return this.upload(file, { ...options, resumable: true });
|
|
486
|
+
}
|
|
317
487
|
};
|
|
318
488
|
|
|
319
489
|
// src/user-org.ts
|
|
@@ -391,7 +561,7 @@ var OrganizationClient = class {
|
|
|
391
561
|
|
|
392
562
|
// src/palette-client.ts
|
|
393
563
|
function createPaletteClient(ctx) {
|
|
394
|
-
const
|
|
564
|
+
const defaultPluginId2 = () => {
|
|
395
565
|
if (ctx?.pluginId) return ctx.pluginId;
|
|
396
566
|
if (typeof window === "undefined") return "";
|
|
397
567
|
const match = window.location.pathname.match(/\/apps\/([^/?#]+)/);
|
|
@@ -401,15 +571,15 @@ function createPaletteClient(ctx) {
|
|
|
401
571
|
user: new UserClient(ctx),
|
|
402
572
|
organization: new OrganizationClient(ctx),
|
|
403
573
|
dataRooms: new DataRoomClient(),
|
|
404
|
-
storage: new StorageClient(),
|
|
574
|
+
storage: new StorageClient(ctx),
|
|
405
575
|
config: {
|
|
406
576
|
get: (pluginId = ctx?.pluginId ?? "") => {
|
|
407
|
-
const resolved = pluginId ||
|
|
577
|
+
const resolved = pluginId || defaultPluginId2();
|
|
408
578
|
if (!resolved) throw new Error("pluginId is required to read install config");
|
|
409
579
|
return getInstallConfig(resolved);
|
|
410
580
|
},
|
|
411
581
|
update: (values, pluginId = ctx?.pluginId ?? "") => {
|
|
412
|
-
const resolved = pluginId ||
|
|
582
|
+
const resolved = pluginId || defaultPluginId2();
|
|
413
583
|
if (!resolved) throw new Error("pluginId is required to update install config");
|
|
414
584
|
return updateInstallConfig(resolved, values);
|
|
415
585
|
}
|