@makolabs/ripple 2.5.3 → 2.5.9

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.
@@ -55,8 +55,10 @@ export class S3Adapter extends BaseAdapter {
55
55
  }
56
56
  async list(path, searchQuery) {
57
57
  try {
58
+ // Use basePath when no path is provided
59
+ const effectivePath = path || this.basePath;
58
60
  // Ensure path ends with a forward slash
59
- const normalizedPath = path.endsWith('/') ? path : path + '/';
61
+ const normalizedPath = effectivePath.endsWith('/') ? effectivePath : effectivePath + '/';
60
62
  // Build API URL including search term and bucket if provided
61
63
  let apiUrl = `/api/s3/list?prefix=${encodeURIComponent(normalizedPath)}`;
62
64
  if (searchQuery) {
@@ -13,6 +13,12 @@ export interface FileItem {
13
13
  id?: string;
14
14
  mimeType?: string;
15
15
  parentId?: string;
16
+ /** Current status of this file in an upload/import flow. */
17
+ status?: 'ready' | 'uploading' | 'success' | 'error';
18
+ /** Upload/import progress (0-100). */
19
+ progress?: number;
20
+ /** Error message when status is 'error'. */
21
+ error?: string;
16
22
  }
17
23
  export interface FileActionSingle {
18
24
  label: (file: FileItem) => string;
@@ -1,5 +1,5 @@
1
1
  import type { Component } from 'svelte';
2
- import type { FileAction, StorageAdapter } from '../adapters/storage/types.js';
2
+ import type { FileAction, FileItem, StorageAdapter } from '../adapters/storage/types.js';
3
3
  export type ChatMessageType = 'chat' | 'action' | 'thinking';
4
4
  export type StreamingCallback = (response: ChatResponse) => void;
5
5
  export interface ChatAction {
@@ -35,6 +35,19 @@ export interface FileBrowserProps {
35
35
  adapter: StorageAdapter;
36
36
  startPath?: string;
37
37
  actions?: FileAction[];
38
+ /**
39
+ * Bindable list of selected files as full FileItem objects. Consumers
40
+ * update `selectedItems[i].status` / `.progress` / `.error` to drive the
41
+ * per-row status icons and inline status summary.
42
+ *
43
+ * Mirrors FileUpload's `files: StagedFile[]` pattern — one unified array
44
+ * for file identity and upload state.
45
+ */
46
+ selectedItems?: FileItem[];
47
+ /**
48
+ * Bindable array of selected file keys. Kept for read access / simple
49
+ * use cases; for status tracking use `selectedItems` instead.
50
+ */
38
51
  selectedFiles?: string[];
39
52
  /** Controls whether the select-all checkbox selects the current page or all data. @default 'page' */
40
53
  selectAllScope?: 'page' | 'all';
@@ -42,8 +55,13 @@ export interface FileBrowserProps {
42
55
  height?: string;
43
56
  /** Additional CSS classes for the outer container. */
44
57
  class?: string;
58
+ /**
59
+ * Custom sidebar snippet. When omitted, the FileBrowser renders a default
60
+ * sidebar with Clear All, batch action buttons, and per-file status icons.
61
+ */
45
62
  infoSection?: (props: {
46
63
  selectedFiles: string[];
64
+ files: FileItem[];
47
65
  navToFileFolder: (fileKey: string) => void;
48
66
  }) => any;
49
67
  testId?: string;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
3
  import { cn } from '../helper/cls.js';
4
- import { Button, Table, Color, Size } from '../index.js';
4
+ import { Button, Table, Color, Size, Spinner } from '../index.js';
5
5
  import type { TableColumn, FileBrowserProps } from '../index.js';
6
6
  import { formatDate } from '../utils/dateUtils.js';
7
7
  import type {
@@ -20,7 +20,8 @@
20
20
  selectAllScope = 'page',
21
21
  height = 'h-[500px]',
22
22
  class: className = '',
23
- selectedFiles = $bindable([])
23
+ selectedItems = $bindable<FileItem[]>([]),
24
+ selectedFiles = $bindable<string[]>([])
24
25
  }: FileBrowserProps = $props();
25
26
 
26
27
  let files = $state<FileItem[]>([]);
@@ -396,6 +397,72 @@
396
397
  let selected = $state<FileItem[]>([]);
397
398
 
398
399
  // Derived: all files to import (not folders), including recursively fetched
400
+ // Sync selectedItems from selectedFiles + fileQueue. Excludes folders
401
+ // (folders are navigation targets, not files to act on — their contents
402
+ // get expanded into fileQueue by handleSelectFolder).
403
+ // Preserves existing FileItem refs so consumer mutations
404
+ // (e.g., selectedItems[i].status = 'uploading') persist across re-syncs.
405
+ $effect(() => {
406
+ // Candidate file keys: selected non-folder files + files from expanded folders
407
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
408
+ const candidateKeys = new Set<string>();
409
+ for (const key of selectedFiles) {
410
+ const item = displayFiles.find((f) => f.key === key);
411
+ if (item && !item.isFolder) candidateKeys.add(key);
412
+ }
413
+ for (const item of fileQueue) {
414
+ if (!item.isFolder) candidateKeys.add(item.key);
415
+ }
416
+
417
+ const existingKeys = new Set(selectedItems.map((f) => f.key));
418
+
419
+ const needsSync =
420
+ selectedItems.some((f) => !candidateKeys.has(f.key)) ||
421
+ [...candidateKeys].some((k) => !existingKeys.has(k));
422
+
423
+ if (!needsSync) return;
424
+
425
+ const kept = selectedItems.filter((f) => candidateKeys.has(f.key));
426
+ const added: FileItem[] = [];
427
+ for (const key of candidateKeys) {
428
+ if (!existingKeys.has(key)) {
429
+ const found =
430
+ displayFiles.find((f) => f.key === key) || fileQueue.find((f) => f.key === key);
431
+ if (found && !found.isFolder) {
432
+ added.push({ ...found, status: found.status ?? 'ready' });
433
+ }
434
+ }
435
+ }
436
+
437
+ selectedItems = [...kept, ...added];
438
+ });
439
+
440
+ // Status tally from selectedItems.
441
+ const statusCounts = $derived.by(() => {
442
+ let uploading = 0;
443
+ let success = 0;
444
+ let error = 0;
445
+ for (const item of selectedItems) {
446
+ if (item.status === 'uploading') uploading++;
447
+ else if (item.status === 'success') success++;
448
+ else if (item.status === 'error') error++;
449
+ }
450
+ return { uploading, success, error };
451
+ });
452
+ const hasUploadActivity = $derived(
453
+ statusCounts.uploading > 0 || statusCounts.success > 0 || statusCounts.error > 0
454
+ );
455
+
456
+ // Auto-clear fileQueue and processedFolders when selectedFiles is emptied
457
+ // from outside (e.g., parent clicks "Clear All" in infoSection).
458
+ $effect(() => {
459
+ if (selectedFiles.length === 0 && fileQueue.length > 0) {
460
+ fileQueue = [];
461
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
462
+ processedFolders = new Set();
463
+ }
464
+ });
465
+
399
466
  const allFilesAcquired = $derived.by(() => {
400
467
  const selectedFileKeys = new Set(selectedFiles);
401
468
 
@@ -706,10 +773,8 @@
706
773
  <Button color={Color.PRIMARY} onclick={authenticate}>Authenticate</Button>
707
774
  </div>
708
775
  {:else}
709
- <div
710
- class="border-default-100 flex flex-wrap items-center justify-between border-b px-4 py-3"
711
- >
712
- <div class="flex flex-wrap items-center">
776
+ <div class="border-default-100 flex items-center gap-3 border-b px-4 py-3">
777
+ <div class="flex min-w-0 flex-1 items-center">
713
778
  {#if breadcrumbs.length > 1}
714
779
  <button
715
780
  class="text-default-600 hover:bg-default-100 mr-1 rounded-full px-2 py-1"
@@ -735,31 +800,55 @@
735
800
  {/if}
736
801
  <span class="text-default-400 mx-1 text-sm">/</span>
737
802
 
738
- <div class="flex flex-wrap items-center">
803
+ <div
804
+ class="flex min-w-0 flex-1 items-center overflow-hidden"
805
+ title={breadcrumbs.map((b) => b.name).join(' / ')}
806
+ >
739
807
  {#each breadcrumbs as crumb, i (crumb.path)}
740
808
  {#if i > 0}
741
- <span class="text-default-400 mx-1 text-sm">/</span>
809
+ <span class="text-default-400 mx-1 shrink-0 text-sm">/</span>
742
810
  {/if}
743
811
 
744
812
  {#if crumb.clickable && !crumb.current}
745
813
  <button
746
- class="text-default-600 hover:bg-default-100 hover:text-default-900 cursor-pointer border-0 bg-transparent px-1 py-0 text-sm"
814
+ class="text-default-600 hover:bg-default-100 hover:text-default-900 max-w-[200px] cursor-pointer truncate border-0 bg-transparent px-1 py-0 text-sm"
747
815
  onclick={() => navigateToFolder(crumb.path)}
748
816
  >
749
817
  {crumb.name}
750
818
  </button>
751
819
  {:else}
752
820
  <span
753
- class={`px-1 text-sm ${crumb.current ? 'text-default-800 font-semibold' : 'text-default-600'}`}
821
+ class={`max-w-[240px] truncate px-1 text-sm ${crumb.current ? 'text-default-800 font-semibold' : 'text-default-600'}`}
754
822
  >
755
823
  {crumb.name}
756
824
  </span>
757
825
  {/if}
758
826
  {/each}
759
827
  </div>
828
+
829
+ {#if selectedFiles.length > 0 && hasUploadActivity}
830
+ <div
831
+ class="text-default-500 ml-3 flex shrink-0 items-center gap-1.5 border-l pl-3 text-xs whitespace-nowrap"
832
+ data-filebrowser-summary=""
833
+ >
834
+ {#if statusCounts.success > 0}
835
+ <span class="text-success-600 font-medium">{statusCounts.success} uploaded</span>
836
+ {/if}
837
+ {#if statusCounts.error > 0}
838
+ {#if statusCounts.success > 0}<span class="text-default-300">·</span>{/if}
839
+ <span class="text-danger-600 font-medium">{statusCounts.error} failed</span>
840
+ {/if}
841
+ {#if statusCounts.uploading > 0}
842
+ {#if statusCounts.success > 0 || statusCounts.error > 0}<span
843
+ class="text-default-300">·</span
844
+ >{/if}
845
+ <span class="text-primary-600 font-medium">{statusCounts.uploading} uploading</span>
846
+ {/if}
847
+ </div>
848
+ {/if}
760
849
  </div>
761
850
 
762
- <div class="flex items-center gap-1">
851
+ <div class="flex shrink-0 items-center gap-1">
763
852
  {#if isFetchingRecursively}
764
853
  <div
765
854
  class="flex items-center gap-2 rounded border border-amber-200 bg-amber-50 px-3 py-1 text-xs"
@@ -866,7 +955,125 @@
866
955
  {#if infoSection}
867
956
  {@render infoSection({
868
957
  selectedFiles: allFilesAcquired.map((file) => file.key),
958
+ files: selectedItems,
869
959
  navToFileFolder
870
960
  })}
961
+ {:else if selectedItems.length > 0}
962
+ {@render defaultInfoSection()}
871
963
  {/if}
872
964
  </div>
965
+
966
+ {#snippet defaultInfoSection()}
967
+ <div class="border-default-200 bg-default-50/50 flex w-72 min-w-72 shrink-0 flex-col border-l">
968
+ <div class="border-default-100 flex flex-col gap-2 border-b px-4 py-3">
969
+ <h3 class="text-default-900 text-sm font-semibold">
970
+ Selected ({selectedItems.length})
971
+ </h3>
972
+ <div class="flex gap-2">
973
+ <Button
974
+ size={Size.XS}
975
+ variant="outline"
976
+ color={Color.DEFAULT}
977
+ onclick={() => {
978
+ selectedFiles = [];
979
+ selectedItems = [];
980
+ }}
981
+ disabled={statusCounts.uploading > 0}
982
+ class="whitespace-nowrap"
983
+ >
984
+ Clear All
985
+ </Button>
986
+ {#each batchActions as action (action.label)}
987
+ <Button
988
+ size={Size.XS}
989
+ color={Color.PRIMARY}
990
+ onclick={() => action.batchAction?.(allFilesAcquired)}
991
+ disabled={!action.isAllowed(allFilesAcquired)}
992
+ class="whitespace-nowrap"
993
+ >
994
+ {action.label(allFilesAcquired)}
995
+ </Button>
996
+ {/each}
997
+ </div>
998
+ </div>
999
+
1000
+ <ul class="divide-default-100 min-h-0 flex-1 divide-y overflow-y-auto">
1001
+ {#each selectedItems as item (item.key)}
1002
+ {@const status = item.status ?? 'ready'}
1003
+ <li
1004
+ class={cn(
1005
+ 'flex items-center gap-2 px-3 py-2 text-xs',
1006
+ status === 'success' && 'opacity-60'
1007
+ )}
1008
+ data-filebrowser-selected-row=""
1009
+ data-status={status}
1010
+ >
1011
+ {#if status === 'uploading'}
1012
+ <div class="flex size-5 shrink-0 items-center justify-center">
1013
+ <Spinner size={Size.XS} color={Color.PRIMARY} />
1014
+ </div>
1015
+ {:else if status === 'success'}
1016
+ <span
1017
+ class="bg-success-100 text-success-600 flex size-5 shrink-0 items-center justify-center rounded-full"
1018
+ aria-hidden="true"
1019
+ >
1020
+ <svg class="size-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1021
+ <path
1022
+ stroke-linecap="round"
1023
+ stroke-linejoin="round"
1024
+ stroke-width="3"
1025
+ d="M5 13l4 4L19 7"
1026
+ />
1027
+ </svg>
1028
+ </span>
1029
+ {:else if status === 'error'}
1030
+ <span
1031
+ class="bg-danger-100 text-danger-600 flex size-5 shrink-0 items-center justify-center rounded-full"
1032
+ aria-hidden="true"
1033
+ >
1034
+ <svg class="size-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1035
+ <path
1036
+ stroke-linecap="round"
1037
+ stroke-linejoin="round"
1038
+ stroke-width="3"
1039
+ d="M6 18L18 6M6 6l12 12"
1040
+ />
1041
+ </svg>
1042
+ </span>
1043
+ {:else}
1044
+ <span class="border-default-300 size-5 shrink-0 rounded-full border" aria-hidden="true"
1045
+ ></span>
1046
+ {/if}
1047
+ <div class="flex min-w-0 flex-1 flex-col">
1048
+ <span class="text-default-900 truncate font-medium" title={item.key}>
1049
+ {item.name}
1050
+ </span>
1051
+ {#if item.error}
1052
+ <span class="text-danger-600 truncate text-[11px]">{item.error}</span>
1053
+ {/if}
1054
+ </div>
1055
+ {#if status !== 'uploading' && status !== 'success'}
1056
+ <button
1057
+ type="button"
1058
+ onclick={() => {
1059
+ selectedFiles = selectedFiles.filter((k) => k !== item.key);
1060
+ }}
1061
+ class="text-default-400 hover:bg-default-100 hover:text-danger-500 shrink-0 cursor-pointer rounded p-1"
1062
+ aria-label="Remove {item.name}"
1063
+ title="Remove"
1064
+ >
1065
+ <svg class="size-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1066
+ <path
1067
+ stroke-linecap="round"
1068
+ stroke-linejoin="round"
1069
+ stroke-width="2"
1070
+ d="M6 18L18 6M6 6l12 12"
1071
+ />
1072
+ </svg>
1073
+ </button>
1074
+ {/if}
1075
+ </li>
1076
+ {/each}
1077
+ </ul>
1078
+ </div>
1079
+ {/snippet}
@@ -1,4 +1,4 @@
1
1
  import type { FileBrowserProps } from '../index.js';
2
- declare const FileBrowser: import("svelte").Component<FileBrowserProps, {}, "selectedFiles">;
2
+ declare const FileBrowser: import("svelte").Component<FileBrowserProps, {}, "selectedItems" | "selectedFiles">;
3
3
  type FileBrowser = ReturnType<typeof FileBrowser>;
4
4
  export default FileBrowser;
@@ -1,5 +1,5 @@
1
1
  import type { User, GetUsersOptions, GetUsersResult } from '../index.js';
2
- export declare const getUsers: import("@sveltejs/kit").RemoteQueryFunction<GetUsersOptions, GetUsersResult>;
2
+ export declare const getUsers: import("@sveltejs/kit").RemoteCommand<GetUsersOptions, Promise<GetUsersResult>>;
3
3
  export declare const createUser: import("@sveltejs/kit").RemoteCommand<Partial<User>, Promise<User>>;
4
4
  export declare const updateUser: import("@sveltejs/kit").RemoteCommand<{
5
5
  userId: string;
@@ -7,7 +7,7 @@ export declare const updateUser: import("@sveltejs/kit").RemoteCommand<{
7
7
  }, Promise<User>>;
8
8
  export declare const deleteUser: import("@sveltejs/kit").RemoteCommand<string, Promise<void>>;
9
9
  export declare const deleteUsers: import("@sveltejs/kit").RemoteCommand<string[], Promise<void>>;
10
- export declare const getUserPermissions: import("@sveltejs/kit").RemoteQueryFunction<string, string[]>;
10
+ export declare const getUserPermissions: import("@sveltejs/kit").RemoteCommand<string, Promise<string[]>>;
11
11
  export declare const updateUserPermissions: import("@sveltejs/kit").RemoteCommand<{
12
12
  userId: string;
13
13
  permissions: string[];
@@ -248,7 +248,7 @@ async function createUserPermissions(email, permissions, clientId = CLIENT_ID) {
248
248
  log.trace('createUserPermissions', 'Result:', result);
249
249
  return result;
250
250
  }
251
- export const getUsers = query('unchecked', async (options) => {
251
+ export const getUsers = command('unchecked', async (options) => {
252
252
  log.trace('getUsers', 'Called with options:', options);
253
253
  try {
254
254
  const limit = options.pageSize;
@@ -491,7 +491,7 @@ async function fetchUserPermissions(email) {
491
491
  }
492
492
  }
493
493
  }
494
- export const getUserPermissions = query('unchecked', async (userId) => {
494
+ export const getUserPermissions = command('unchecked', async (userId) => {
495
495
  log.trace('getUserPermissions', 'Called for userId:', userId);
496
496
  try {
497
497
  // Fetch user from Clerk to get email
@@ -0,0 +1 @@
1
+ export * from './s3.js';
@@ -0,0 +1 @@
1
+ export * from './s3.js';
@@ -0,0 +1,53 @@
1
+ /**
2
+ * S3 server handler factory. Returns Request handlers that the client-side
3
+ * S3Adapter expects at /api/s3/list and /api/s3/download.
4
+ *
5
+ * Keeps AWS credentials server-side — consumers just wire these handlers into
6
+ * their route files.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // src/routes/api/s3/list/+server.ts
11
+ * import { createS3Handlers } from '@makolabs/ripple/server';
12
+ * import { env } from '$env/dynamic/private';
13
+ *
14
+ * const s3 = createS3Handlers({
15
+ * bucket: env.S3_BUCKET,
16
+ * region: env.S3_REGION,
17
+ * accessKeyId: env.S3_ACCESS_KEY_ID,
18
+ * secretAccessKey: env.S3_SECRET_ACCESS_KEY,
19
+ * endpoint: env.S3_ENDPOINT, // optional, e.g. for DO Spaces
20
+ * });
21
+ *
22
+ * export const GET = ({ request }) => s3.list(request);
23
+ * ```
24
+ */
25
+ export interface S3HandlerConfig {
26
+ /** S3 bucket name (default used when request doesn't specify one) */
27
+ bucket: string;
28
+ /** AWS region or DO Spaces region (e.g. 'us-east-1', 'fra1') */
29
+ region: string;
30
+ /** AWS access key ID */
31
+ accessKeyId: string;
32
+ /** AWS secret access key */
33
+ secretAccessKey: string;
34
+ /**
35
+ * Custom endpoint URL for S3-compatible services like DigitalOcean Spaces.
36
+ * e.g. 'https://fra1.digitaloceanspaces.com'
37
+ */
38
+ endpoint?: string;
39
+ /**
40
+ * Whether a specific bucket override passed via the `bucket` query param
41
+ * should be allowed. Defaults to `false` for safety — only the configured
42
+ * bucket is accessible. Set to `true` if you need multi-bucket support.
43
+ */
44
+ allowBucketOverride?: boolean;
45
+ /**
46
+ * Download URL expiry in seconds. @default 3600 (1 hour)
47
+ */
48
+ downloadUrlExpirySeconds?: number;
49
+ }
50
+ export declare function createS3Handlers(config: S3HandlerConfig): {
51
+ list: (request: Request) => Promise<Response>;
52
+ download: (request: Request) => Promise<Response>;
53
+ };
@@ -0,0 +1,100 @@
1
+ import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3';
2
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
3
+ export function createS3Handlers(config) {
4
+ const clientConfig = {
5
+ region: config.region,
6
+ credentials: {
7
+ accessKeyId: config.accessKeyId,
8
+ secretAccessKey: config.secretAccessKey
9
+ }
10
+ };
11
+ if (config.endpoint) {
12
+ clientConfig.endpoint = config.endpoint;
13
+ clientConfig.forcePathStyle = false; // DO Spaces uses virtual-hosted style
14
+ }
15
+ const client = new S3Client(clientConfig);
16
+ const expirySeconds = config.downloadUrlExpirySeconds ?? 3600;
17
+ /**
18
+ * GET /api/s3/list?prefix=<path>&search=<term>&bucket=<name>
19
+ *
20
+ * Returns folders and files at the given prefix.
21
+ */
22
+ async function list(request) {
23
+ try {
24
+ const url = new URL(request.url);
25
+ const prefix = url.searchParams.get('prefix') ?? '';
26
+ const search = url.searchParams.get('search') ?? '';
27
+ const bucketParam = url.searchParams.get('bucket');
28
+ const bucket = config.allowBucketOverride && bucketParam ? bucketParam : config.bucket;
29
+ const command = new ListObjectsV2Command({
30
+ Bucket: bucket,
31
+ Prefix: prefix,
32
+ Delimiter: '/'
33
+ });
34
+ const response = await client.send(command);
35
+ // Folders come back as CommonPrefixes
36
+ let folders = (response.CommonPrefixes ?? []).map((cp) => ({
37
+ prefix: cp.Prefix ?? '',
38
+ folderName: cp.Prefix ? cp.Prefix.slice(prefix.length).replace(/\/$/, '') : ''
39
+ }));
40
+ // Files come back as Contents
41
+ let files = (response.Contents ?? [])
42
+ .filter((obj) => obj.Key && obj.Key !== prefix)
43
+ .map((obj) => ({
44
+ key: obj.Key,
45
+ lastModified: obj.LastModified?.toISOString() ?? new Date().toISOString(),
46
+ size: obj.Size ?? 0
47
+ }));
48
+ // Apply search filter if provided (case-insensitive substring match on name)
49
+ if (search) {
50
+ const needle = search.toLowerCase();
51
+ folders = folders.filter((f) => f.folderName.toLowerCase().includes(needle));
52
+ files = files.filter((f) => {
53
+ const name = f.key.slice(prefix.length).toLowerCase();
54
+ return name.includes(needle);
55
+ });
56
+ }
57
+ return jsonResponse({ folders, files });
58
+ }
59
+ catch (err) {
60
+ return errorResponse(err);
61
+ }
62
+ }
63
+ /**
64
+ * GET /api/s3/download?key=<object-key>&bucket=<name>
65
+ *
66
+ * Returns a presigned URL (as plain text) that the browser can GET to
67
+ * download the file.
68
+ */
69
+ async function download(request) {
70
+ try {
71
+ const url = new URL(request.url);
72
+ const key = url.searchParams.get('key');
73
+ const bucketParam = url.searchParams.get('bucket');
74
+ if (!key) {
75
+ return jsonResponse({ error: 'Missing required "key" query param' }, 400);
76
+ }
77
+ const bucket = config.allowBucketOverride && bucketParam ? bucketParam : config.bucket;
78
+ const command = new GetObjectCommand({ Bucket: bucket, Key: key });
79
+ const signedUrl = await getSignedUrl(client, command, { expiresIn: expirySeconds });
80
+ return new Response(signedUrl, {
81
+ status: 200,
82
+ headers: { 'Content-Type': 'text/plain' }
83
+ });
84
+ }
85
+ catch (err) {
86
+ return errorResponse(err);
87
+ }
88
+ }
89
+ return { list, download };
90
+ }
91
+ function jsonResponse(body, status = 200) {
92
+ return new Response(JSON.stringify(body), {
93
+ status,
94
+ headers: { 'Content-Type': 'application/json' }
95
+ });
96
+ }
97
+ function errorResponse(err) {
98
+ const message = err instanceof Error ? err.message : 'Unknown error';
99
+ return jsonResponse({ error: message }, 500);
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makolabs/ripple",
3
- "version": "2.5.3",
3
+ "version": "2.5.9",
4
4
  "description": "Simple Svelte 5 powered component library ✨",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
@@ -42,6 +42,10 @@
42
42
  "types": "./dist/index.d.ts",
43
43
  "svelte": "./dist/index.js"
44
44
  },
45
+ "./server": {
46
+ "types": "./dist/server/index.d.ts",
47
+ "default": "./dist/server/index.js"
48
+ },
45
49
  "./funcs/*": {
46
50
  "types": "./dist/funcs/*.d.ts",
47
51
  "default": "./dist/funcs/*.js"
@@ -121,6 +125,8 @@
121
125
  "*.{js,ts,svelte,css,md,json}": "prettier --write"
122
126
  },
123
127
  "dependencies": {
128
+ "@aws-sdk/client-s3": "^3.1029.0",
129
+ "@aws-sdk/s3-request-presigner": "^3.1029.0",
124
130
  "@friendofsvelte/mermaid": "^0.0.4",
125
131
  "dayjs": "^1.11.19",
126
132
  "echarts": "^6.0.0",