@kmlckj/licos-platform-sdk 0.5.0 → 0.6.1

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/src/shared.js CHANGED
@@ -10,249 +10,3 @@ export class PlatformSdkError extends Error {
10
10
 
11
11
  export class ConfigurationError extends PlatformSdkError {}
12
12
  export class ApiError extends PlatformSdkError {}
13
-
14
- export function isNodeRuntime() {
15
- return typeof process !== 'undefined' && !!process?.versions?.node;
16
- }
17
-
18
- function readEnv(name) {
19
- if (typeof process === 'undefined' || !process?.env) {
20
- return undefined;
21
- }
22
- const value = process.env[name];
23
- return value ? value : undefined;
24
- }
25
-
26
- export function requireEnv(name) {
27
- const value = readEnv(name);
28
- if (value) {
29
- return value;
30
- }
31
- throw new ConfigurationError(`Missing required environment variable: ${name}`);
32
- }
33
-
34
- export function normalizeBaseUrl(baseUrl) {
35
- if (typeof baseUrl === 'string') {
36
- return baseUrl.replace(/\/+$/, '');
37
- }
38
-
39
- const envBaseUrl = readEnv('LICOS_ORCHESTRATOR_API_BASE_URL');
40
- if (envBaseUrl) {
41
- return envBaseUrl.replace(/\/+$/, '');
42
- }
43
-
44
- if (!isNodeRuntime()) {
45
- return '';
46
- }
47
-
48
- throw new ConfigurationError(
49
- 'Missing required environment variable: LICOS_ORCHESTRATOR_API_BASE_URL',
50
- );
51
- }
52
-
53
- export function resolveUserId(userId) {
54
- return userId ?? requireEnv('LICOS_USER_ID');
55
- }
56
-
57
- export function resolveProjectId(projectId) {
58
- return projectId ?? requireEnv('LICOS_PROJECT_ID');
59
- }
60
-
61
- export function encodeStorageSegment(value) {
62
- return encodeURIComponent(value);
63
- }
64
-
65
- export function encodeObjectKey(value) {
66
- return value.split('/').map(encodeStorageSegment).join('/');
67
- }
68
-
69
- export function fileNameFromPath(filePath) {
70
- const parts = String(filePath).split(/[\\/]/);
71
- const fileName = parts[parts.length - 1];
72
- return fileName || 'upload.bin';
73
- }
74
-
75
- export function guessContentType(fileName) {
76
- const ext = /\.[^./\\]+$/.exec(String(fileName).toLowerCase())?.[0] ?? '';
77
- switch (ext) {
78
- case '.txt':
79
- return 'text/plain';
80
- case '.json':
81
- return 'application/json';
82
- case '.pdf':
83
- return 'application/pdf';
84
- case '.png':
85
- return 'image/png';
86
- case '.jpg':
87
- case '.jpeg':
88
- return 'image/jpeg';
89
- case '.gif':
90
- return 'image/gif';
91
- case '.webp':
92
- return 'image/webp';
93
- case '.svg':
94
- return 'image/svg+xml';
95
- case '.md':
96
- return 'text/markdown';
97
- default:
98
- return 'application/octet-stream';
99
- }
100
- }
101
-
102
- function isBlobLike(value) {
103
- return !!value && typeof value === 'object' && typeof value.arrayBuffer === 'function';
104
- }
105
-
106
- function isBinaryDescriptor(value) {
107
- if (!value || typeof value !== 'object' || isBlobLike(value)) {
108
- return false;
109
- }
110
- return 'data' in value || 'file' in value;
111
- }
112
-
113
- function toUint8Array(data) {
114
- if (data instanceof Uint8Array) {
115
- return data;
116
- }
117
- if (ArrayBuffer.isView(data)) {
118
- return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
119
- }
120
- return new Uint8Array(data);
121
- }
122
-
123
- async function normalizeBinaryPayload(data, overrides = {}) {
124
- if (isBlobLike(data)) {
125
- const fileName =
126
- overrides.fileName ??
127
- (typeof data.name === 'string' && data.name ? data.name : undefined) ??
128
- 'upload.bin';
129
- const contentType =
130
- overrides.contentType ??
131
- (typeof data.type === 'string' && data.type ? data.type : undefined) ??
132
- guessContentType(fileName);
133
- return {
134
- fileName,
135
- contentType,
136
- blob: new Blob([await data.arrayBuffer()], { type: contentType }),
137
- };
138
- }
139
-
140
- if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
141
- const fileName = overrides.fileName ?? 'upload.bin';
142
- const contentType = overrides.contentType ?? guessContentType(fileName);
143
- return {
144
- fileName,
145
- contentType,
146
- blob: new Blob([toUint8Array(data)], { type: contentType }),
147
- };
148
- }
149
-
150
- throw new ConfigurationError(
151
- 'Unsupported upload input. Use a file path string, Blob/File, ArrayBuffer, TypedArray, or { data, fileName }.',
152
- );
153
- }
154
-
155
- export async function normalizeUploadSource(source, readPathSource) {
156
- if (typeof source === 'string') {
157
- if (!readPathSource) {
158
- throw new ConfigurationError(
159
- 'Front-end mode does not support filesystem path uploads. Pass File, Blob, ArrayBuffer, TypedArray, or { data, fileName } instead.',
160
- );
161
- }
162
- return readPathSource(source);
163
- }
164
-
165
- if (isBinaryDescriptor(source)) {
166
- return normalizeBinaryPayload(source.data ?? source.file, {
167
- fileName: source.fileName,
168
- contentType: source.contentType,
169
- });
170
- }
171
-
172
- return normalizeBinaryPayload(source);
173
- }
174
-
175
- function headersFromInit(headersInit) {
176
- return new Headers(headersInit ?? undefined);
177
- }
178
-
179
- export function buildRequestHeaders(options = {}, { authenticated = false } = {}) {
180
- const headers = headersFromInit(options.headers);
181
- if (authenticated && !headers.has('Authorization')) {
182
- const resolvedToken = options.token ?? readEnv('LICOS_USER_TOKEN');
183
- if (resolvedToken) {
184
- headers.set('Authorization', `Bearer ${resolvedToken}`);
185
- } else if (isNodeRuntime()) {
186
- throw new ConfigurationError(
187
- 'Missing authentication token. Pass options.token, provide an Authorization header, or set LICOS_USER_TOKEN.',
188
- );
189
- }
190
- }
191
- return headers;
192
- }
193
-
194
- export function buildRequestInit(options = {}, init = {}) {
195
- const requestInit = { ...init };
196
- if (options.credentials) {
197
- requestInit.credentials = options.credentials;
198
- }
199
- return requestInit;
200
- }
201
-
202
- export async function readJsonEnvelope(response) {
203
- let payload;
204
- try {
205
- payload = await response.json();
206
- } catch (error) {
207
- throw new ApiError('Platform API returned non-JSON response', {
208
- status: response.status,
209
- details: error instanceof Error ? error.message : String(error),
210
- });
211
- }
212
-
213
- if (!response.ok || payload?.code !== 0) {
214
- throw new ApiError(payload?.message ?? `Platform API failed: HTTP ${response.status}`, {
215
- status: response.status,
216
- code: payload?.code,
217
- details: payload?.data ?? payload,
218
- });
219
- }
220
- return payload.data;
221
- }
222
-
223
- export async function downloadBlob(fetchImpl, url, options = {}) {
224
- const response = await fetchImpl(
225
- url,
226
- buildRequestInit(options, {
227
- method: 'GET',
228
- headers: buildRequestHeaders(options, { authenticated: true }),
229
- }),
230
- );
231
- if (!response.ok) {
232
- throw new ApiError(`Platform download failed: HTTP ${response.status}`, {
233
- status: response.status,
234
- });
235
- }
236
- if (typeof response.blob === 'function') {
237
- return response.blob();
238
- }
239
- return new Blob([await response.arrayBuffer()], {
240
- type: response.headers.get('content-type') ?? undefined,
241
- });
242
- }
243
-
244
- export async function uploadFile(fetchImpl, url, source, options = {}, readPathSource) {
245
- const normalized = await normalizeUploadSource(source, readPathSource);
246
- const formData = new FormData();
247
- formData.append('file', normalized.blob, normalized.fileName);
248
-
249
- const response = await fetchImpl(
250
- url,
251
- buildRequestInit(options, {
252
- method: 'POST',
253
- headers: buildRequestHeaders(options, { authenticated: true }),
254
- body: formData,
255
- }),
256
- );
257
- return readJsonEnvelope(response);
258
- }
package/src/storage.js ADDED
@@ -0,0 +1,155 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { ApiError } from './shared.js';
5
+ import { authHeaders, runtimeConfig } from './runtime.js';
6
+
7
+ function storageConfig() {
8
+ return runtimeConfig({ environmentOverrideEnv: 'LICOS_STORAGE_SCOPE' });
9
+ }
10
+
11
+ function query(config, params = {}) {
12
+ const search = new URLSearchParams();
13
+ search.set('projectId', config.projectId);
14
+ search.set('scope', config.environment);
15
+ for (const [key, value] of Object.entries(params)) {
16
+ if (value !== undefined && value !== null) search.set(key, String(value));
17
+ }
18
+ return search.toString();
19
+ }
20
+
21
+ function storageUrl(config, route, params) {
22
+ return `${config.baseUrl}/api/v1/studio/storage${route}?${query(config, params)}`;
23
+ }
24
+
25
+ async function decodeResponse(response) {
26
+ const text = await response.text();
27
+ const payload = text ? JSON.parse(text) : null;
28
+ if (!response.ok) {
29
+ throw new ApiError(text || `platform storage API returned ${response.status}`, {
30
+ status: response.status,
31
+ details: payload,
32
+ });
33
+ }
34
+ if (!payload || typeof payload !== 'object') return payload;
35
+ if ((payload.code !== undefined && payload.code !== 0) || payload.success === false) {
36
+ throw new ApiError(payload.message || 'platform storage API failed', {
37
+ status: response.status,
38
+ code: typeof payload.code === 'number' ? payload.code : undefined,
39
+ details: payload,
40
+ });
41
+ }
42
+ return payload.data;
43
+ }
44
+
45
+ async function requestJson(method, route, { params, body } = {}) {
46
+ const config = storageConfig();
47
+ const response = await fetch(storageUrl(config, route, params), {
48
+ method,
49
+ headers: authHeaders(config),
50
+ body: body === undefined ? undefined : JSON.stringify(body),
51
+ });
52
+ return decodeResponse(response);
53
+ }
54
+
55
+ async function filePart(input, options = {}) {
56
+ if (typeof input === 'string') {
57
+ const data = await readFile(input);
58
+ return {
59
+ blob: new Blob([data], { type: options.contentType || 'application/octet-stream' }),
60
+ fileName: options.fileName || path.basename(input),
61
+ };
62
+ }
63
+ if (input instanceof Blob) {
64
+ return { blob: input, fileName: options.fileName || 'file' };
65
+ }
66
+ if (input instanceof Uint8Array || Buffer.isBuffer(input)) {
67
+ return {
68
+ blob: new Blob([input], { type: options.contentType || 'application/octet-stream' }),
69
+ fileName: options.fileName || 'file',
70
+ };
71
+ }
72
+ throw new TypeError('uploadFile input must be a file path, Blob, Buffer, or Uint8Array');
73
+ }
74
+
75
+ export async function summary() {
76
+ return requestJson('GET', '/summary');
77
+ }
78
+
79
+ export async function listFolders() {
80
+ return (await requestJson('GET', '/folders')) || [];
81
+ }
82
+
83
+ export async function listEntries({ folderId } = {}) {
84
+ return requestJson('GET', '/entries', { params: { folderId } });
85
+ }
86
+
87
+ export async function listFiles() {
88
+ return (await requestJson('GET', '/files')) || [];
89
+ }
90
+
91
+ export async function createFolder(folderName, { parentId } = {}) {
92
+ return requestJson('POST', '/folders', { body: { folderName, parentId } });
93
+ }
94
+
95
+ export async function renameFolder(folderId, folderName) {
96
+ return requestJson('PUT', `/folders/${encodeURIComponent(folderId)}`, { body: { folderName } });
97
+ }
98
+
99
+ export async function deleteFolder(folderId) {
100
+ await requestJson('DELETE', `/folders/${encodeURIComponent(folderId)}`);
101
+ }
102
+
103
+ export async function uploadFile(input, options = {}) {
104
+ const config = storageConfig();
105
+ const { blob, fileName } = await filePart(input, options);
106
+ const form = new FormData();
107
+ form.set('projectId', config.projectId);
108
+ form.set('scope', config.environment);
109
+ if (options.folderId) form.set('folderId', options.folderId);
110
+ form.set('file', blob, fileName);
111
+ const response = await fetch(`${config.baseUrl}/api/v1/studio/storage/files/upload`, {
112
+ method: 'POST',
113
+ headers: authHeaders(config, undefined),
114
+ body: form,
115
+ });
116
+ return decodeResponse(response);
117
+ }
118
+
119
+ export async function renameFile(fileId, fileName) {
120
+ return requestJson('PUT', `/files/${encodeURIComponent(fileId)}/rename`, { body: { fileName } });
121
+ }
122
+
123
+ export async function moveFile(fileId, { folderId } = {}) {
124
+ return requestJson('PUT', `/files/${encodeURIComponent(fileId)}/move`, { body: { folderId } });
125
+ }
126
+
127
+ export async function shareUrl(fileId, { expirySeconds } = {}) {
128
+ return requestJson('POST', `/files/${encodeURIComponent(fileId)}/share-url`, {
129
+ body: { expirySeconds },
130
+ });
131
+ }
132
+
133
+ export async function objectExists({ fileId, objectKey } = {}) {
134
+ return requestJson('POST', '/objects/exists', { body: { fileId, objectKey } });
135
+ }
136
+
137
+ export async function deleteFile(fileId) {
138
+ await requestJson('DELETE', `/files/${encodeURIComponent(fileId)}`);
139
+ }
140
+
141
+ export const storage = {
142
+ summary,
143
+ listFolders,
144
+ listEntries,
145
+ listFiles,
146
+ createFolder,
147
+ renameFolder,
148
+ deleteFolder,
149
+ uploadFile,
150
+ renameFile,
151
+ moveFile,
152
+ shareUrl,
153
+ objectExists,
154
+ deleteFile,
155
+ };