@fenaura/sdk 0.2.2 → 0.2.3
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/bin/init.js +18 -18
- package/js/fenaura-client.d.ts +3 -3
- package/js/fenaura-client.js +25 -25
- package/package.json +2 -2
package/bin/init.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// npx @fenaura/auth-init
|
|
2
|
+
// npx @fenaura/auth-init - run from your frontend folder.
|
|
3
3
|
// Writes the /fenaura/* proxy files your host needs. Idempotent.
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
@@ -17,7 +17,7 @@ async function confirmRoot() {
|
|
|
17
17
|
const answer = await new Promise((resolve) => rl.question(`No package.json here. Is this your frontend root? (${cwd}) [Y/n] `, resolve));
|
|
18
18
|
rl.close();
|
|
19
19
|
if (!/^(y|yes)?\s*$/i.test(answer || '')) {
|
|
20
|
-
console.error('Aborted
|
|
20
|
+
console.error('Aborted - cd into your frontend folder first, then re-run.');
|
|
21
21
|
process.exit(1);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -87,7 +87,7 @@ async function main() {
|
|
|
87
87
|
const vp = path.join(cwd, viteCfg);
|
|
88
88
|
let src = fs.readFileSync(vp, 'utf8');
|
|
89
89
|
if (src.includes('/fenaura')) {
|
|
90
|
-
// already has proxy
|
|
90
|
+
// already has proxy - correct it if API mismatched (tampered)
|
|
91
91
|
if (src.includes(API)) {
|
|
92
92
|
report.push(`kept ${viteCfg} (proxy already present)`);
|
|
93
93
|
} else {
|
|
@@ -98,26 +98,26 @@ async function main() {
|
|
|
98
98
|
report.push(`fixed ${viteCfg} (corrected tampered API target → ${API})`);
|
|
99
99
|
}
|
|
100
100
|
} else if (/server\s*:\s*\{[^}]*proxy\s*:/.test(src)) {
|
|
101
|
-
// server.proxy exists
|
|
101
|
+
// server.proxy exists - append our entry inside it
|
|
102
102
|
src = src.replace(/(proxy\s*:\s*\{)/, `$1\n ${PROXY_BLOCK},`);
|
|
103
103
|
fs.writeFileSync(vp, src);
|
|
104
104
|
report.push(`wrote ${viteCfg} (appended to server.proxy)`);
|
|
105
105
|
} else if (/defineConfig\(\s*\{/.test(src)) {
|
|
106
|
-
// no server key
|
|
106
|
+
// no server key - add one
|
|
107
107
|
src = src.replace(/(defineConfig\(\s*\{)/, `$1\n server: {\n proxy: {\n ${PROXY_BLOCK},\n },\n },`);
|
|
108
108
|
fs.writeFileSync(vp, src);
|
|
109
109
|
report.push(`wrote ${viteCfg} (added server.proxy)`);
|
|
110
110
|
} else {
|
|
111
|
-
report.push(`${viteCfg} has no defineConfig({...)
|
|
111
|
+
report.push(`${viteCfg} has no defineConfig({...) - add manually:`);
|
|
112
112
|
report.push(` ${PROXY_BLOCK}`);
|
|
113
113
|
}
|
|
114
114
|
} else if (nextCfg) {
|
|
115
|
-
report.push('next detected
|
|
115
|
+
report.push('next detected - rewrites() needs an async function; add manually:');
|
|
116
116
|
report.push(` { source: '/fenaura/:path*', destination: '${API}/:path*' }`);
|
|
117
117
|
} else if (fallbackVite) {
|
|
118
118
|
// SvelteKit / Astro / Nuxt: they all proxy via vite underneath, but config shape varies
|
|
119
119
|
// For these, ensure public/_redirects covers prod and print the vite snippet for dev
|
|
120
|
-
report.push(`${fallbackVite} detected (vite-based)
|
|
120
|
+
report.push(`${fallbackVite} detected (vite-based) - add to vite server.proxy for dev:`);
|
|
121
121
|
report.push(` ${PROXY_BLOCK}`);
|
|
122
122
|
report.push(' (public/_redirects already covers prod)');
|
|
123
123
|
} else {
|
|
@@ -142,7 +142,7 @@ async function main() {
|
|
|
142
142
|
} else {
|
|
143
143
|
proxy.enable = true;
|
|
144
144
|
proxy.baseUri = '/fenaura';
|
|
145
|
-
// Via local nginx (:3000), which strips /fenaura
|
|
145
|
+
// Via local nginx (:3000), which strips /fenaura - Live Server can't rewrite paths itself.
|
|
146
146
|
proxy.proxyUri = 'http://localhost:3000/fenaura';
|
|
147
147
|
fs.mkdirSync(vsDir, { recursive: true });
|
|
148
148
|
fs.writeFileSync(vsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
@@ -151,9 +151,9 @@ async function main() {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
const wroteAny = report.some(r => r.startsWith('wrote') || r.startsWith('fixed'));
|
|
154
|
-
console.log('\n' + (wroteAny ? '✓ Fenaura auth proxy setup
|
|
155
|
-
if (wroteAny) console.log('\n → You should see "wrote public/_redirects" and "wrote vercel.json" above (or "fixed …" if a tampered file was corrected). If you see "kept … (already present)" it was already configured
|
|
156
|
-
else console.log('\n → Nothing to write
|
|
154
|
+
console.log('\n' + (wroteAny ? '✓ Fenaura auth proxy setup - done' : '✓ Fenaura auth proxy setup - already configured') + '\n' + report.map((r) => ' ' + r).join('\n'));
|
|
155
|
+
if (wroteAny) console.log('\n → You should see "wrote public/_redirects" and "wrote vercel.json" above (or "fixed …" if a tampered file was corrected). If you see "kept … (already present)" it was already configured - you are good to go.');
|
|
156
|
+
else console.log('\n → Nothing to write - proxy files were already present. You are good to go.');
|
|
157
157
|
console.log('\nFrontend calls (copy-paste, project ID from dashboard):');
|
|
158
158
|
console.log(" fetch('/fenaura/auth/dp/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, password, project_id: '<PROJECT_ID>' }) })");
|
|
159
159
|
|
|
@@ -185,13 +185,13 @@ async function main() {
|
|
|
185
185
|
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
186
186
|
const hasSdk = deps['@fenaura/client'] || deps['@fenaura/sdk'] || deps['fenaura'];
|
|
187
187
|
if (!hasSdk) {
|
|
188
|
-
console.log('\n → SDK not in package.json
|
|
188
|
+
console.log('\n → SDK not in package.json - installing @fenaura/sdk ...');
|
|
189
189
|
const { spawnSync } = require('child_process');
|
|
190
190
|
const res = spawnSync('npm', ['install', '@fenaura/sdk', '--save'], { stdio: 'inherit', cwd });
|
|
191
|
-
if (res.status !== 0) console.log(' (auto-install failed
|
|
191
|
+
if (res.status !== 0) console.log(' (auto-install failed - run: npm install @fenaura/sdk)');
|
|
192
192
|
else console.log(' ✓ @fenaura/client installed');
|
|
193
193
|
}
|
|
194
|
-
} catch { /* no package.json or parse fail
|
|
194
|
+
} catch { /* no package.json or parse fail - skip auto-install */ }
|
|
195
195
|
|
|
196
196
|
// 6. Detect how the dev server is running + print next steps.
|
|
197
197
|
const net = require('net');
|
|
@@ -213,10 +213,10 @@ async function main() {
|
|
|
213
213
|
}
|
|
214
214
|
console.log('\nNext steps:');
|
|
215
215
|
console.log(` 1. Start dev: ${startCmd} (detected stack: ${stack})`);
|
|
216
|
-
console.log(` 2. Open the page (local ports answering now: ${open.length ? open.join(', ') : 'none yet
|
|
217
|
-
console.log(' 3. Click login
|
|
216
|
+
console.log(` 2. Open the page (local ports answering now: ${open.length ? open.join(', ') : 'none yet - start the server first'})`);
|
|
217
|
+
console.log(' 3. Click login - expect session_minted + a fenaura_eusid_* cookie on YOUR domain (devtools → Application → Cookies).');
|
|
218
218
|
console.log(' 4. Public URL test: forward the dev port in VS Code (Ports → Public, protocol http) and open the tunnel link.');
|
|
219
|
-
console.log(' 5. Deploy: the _redirects/vercel.json files proxy /fenaura/* in prod
|
|
219
|
+
console.log(' 5. Deploy: the _redirects/vercel.json files proxy /fenaura/* in prod - no code changes.\n');
|
|
220
220
|
})();
|
|
221
221
|
|
|
222
222
|
}
|
package/js/fenaura-client.d.ts
CHANGED
|
@@ -157,7 +157,7 @@ interface CompressResult {
|
|
|
157
157
|
mode: string;
|
|
158
158
|
originalSize: number;
|
|
159
159
|
compressedSize: number;
|
|
160
|
-
/** Real MIME of `file`
|
|
160
|
+
/** Real MIME of `file` - never assumed, always the produced bytes' type */
|
|
161
161
|
contentType: string;
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -223,7 +223,7 @@ interface QueryBuilder<T = Record<string, any>> {
|
|
|
223
223
|
select(columns?: string): QueryBuilder<T>;
|
|
224
224
|
insert(data: T | T[]): Promise<QueryResult & { data: T[] }>;
|
|
225
225
|
update(data: Partial<T>): QueryBuilder<T>;
|
|
226
|
-
/** Single object only
|
|
226
|
+
/** Single object only - arrays are rejected 400 server-side. */
|
|
227
227
|
upsert(data: T): Promise<QueryResult & { data: T[] }>;
|
|
228
228
|
delete(): QueryBuilder<T>;
|
|
229
229
|
|
|
@@ -311,7 +311,7 @@ interface StorageBucket {
|
|
|
311
311
|
download(path: string): Promise<QueryResult & { data: Blob }>;
|
|
312
312
|
list(path?: string, options?: ListOptions): Promise<QueryResult & { data: FileObject[] }>;
|
|
313
313
|
remove(paths: string[]): Promise<QueryResult>;
|
|
314
|
-
/** Same-origin SESSION url (not public
|
|
314
|
+
/** Same-origin SESSION url (not public - GET needs the caller's cookie). Segments encoded. Outsiders: createSignedUrl. */
|
|
315
315
|
getPublicUrl(path: string): string;
|
|
316
316
|
/** Real HMAC-signed expiring link (server-enforced, ≤7d). Anonymous fetches OK until exp. */
|
|
317
317
|
createSignedUrl(path: string, expiresIn?: number): Promise<QueryResult & { data: { signedUrl: string } }>;
|
package/js/fenaura-client.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* // Storage
|
|
20
20
|
* await db.storage.from('avatars').upload('user1.jpg', file)
|
|
21
21
|
*
|
|
22
|
-
* // Storage with client-side compression (images shrink, text gzips
|
|
22
|
+
* // Storage with client-side compression (images shrink, text gzips -
|
|
23
23
|
* * fewer bytes leave the device over the same chunked upload)
|
|
24
24
|
* await db.storage.from('avatars').upload('user1.jpg', file, {
|
|
25
25
|
* compress: { mode: 'auto', maxWidth: 1920, quality: 0.8, format: 'webp' },
|
|
@@ -337,7 +337,7 @@ class QueryBuilder {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
/**
|
|
340
|
-
* Upsert data (insert or update)
|
|
340
|
+
* Upsert data (insert or update) - single object only (D18: arrays are
|
|
341
341
|
* rejected 400 server-side; bulk upsert is insert-then-update instead).
|
|
342
342
|
* @param {Object} data - Data to upsert
|
|
343
343
|
* @returns {Promise<QueryResult>}
|
|
@@ -727,10 +727,10 @@ class AuthClient {
|
|
|
727
727
|
* @param {Object} params
|
|
728
728
|
* @param {string} params.email - Email address
|
|
729
729
|
* @param {string} params.password - Password
|
|
730
|
-
* @param {Object} [options] - { browser=true }
|
|
730
|
+
* @param {Object} [options] - { browser=true } - browser:false returns the raw
|
|
731
731
|
* token in `data.token` (no cookie set) for non-web clients / backends.
|
|
732
732
|
* Store or forward it yourself. WARNING: a raw token in JS memory or
|
|
733
|
-
* localStorage is readable by any script on the page
|
|
733
|
+
* localStorage is readable by any script on the page - prefer the cookie
|
|
734
734
|
* flow in browsers, and never use browser:false on shared machines.
|
|
735
735
|
* @returns {Promise<QueryResult>}
|
|
736
736
|
*/
|
|
@@ -749,7 +749,7 @@ class AuthClient {
|
|
|
749
749
|
* @param {Object} params
|
|
750
750
|
* @param {string} params.email - Email address
|
|
751
751
|
* @param {string} params.password - Password
|
|
752
|
-
* @param {Object} [options] - { browser=true }
|
|
752
|
+
* @param {Object} [options] - { browser=true } - browser:false returns the raw
|
|
753
753
|
* token in `data.token` (no cookie set) for non-web clients / backends.
|
|
754
754
|
* @returns {Promise<QueryResult>}
|
|
755
755
|
*/
|
|
@@ -785,7 +785,7 @@ class AuthClient {
|
|
|
785
785
|
* Sign in with OAuth provider (Supabase-shaped, Fenaura transport).
|
|
786
786
|
* Web (default): redirects the browser to the provider; session returns as
|
|
787
787
|
* HttpOnly fenaura_eusid_* cookie (+ one-time ?code= for exchange).
|
|
788
|
-
* Non-web ({ browser:false }): no navigation
|
|
788
|
+
* Non-web ({ browser:false }): no navigation - returns { data:{url} };
|
|
789
789
|
* open the URL in a system browser, then call exchangeCodeForSession(code)
|
|
790
790
|
* with the ?code= your redirect_to (deep link) receives.
|
|
791
791
|
* @param {string} provider - google|github|microsoft|apple|facebook|twitter|discord|linkedin|spotify|slack|gitlab|twitch
|
|
@@ -803,7 +803,7 @@ class AuthClient {
|
|
|
803
803
|
if (redirectTo) q.set('redirect_to', redirectTo);
|
|
804
804
|
if (scopes) q.set('scopes', scopes);
|
|
805
805
|
if (queryParams && Object.keys(queryParams).length > 0) {
|
|
806
|
-
// Global AJV removeAdditional strips unknown query keys (server.ts)
|
|
806
|
+
// Global AJV removeAdditional strips unknown query keys (server.ts) -
|
|
807
807
|
// send extras as one JSON blob `qp`, server filters reserved keys.
|
|
808
808
|
q.set('qp', JSON.stringify(queryParams));
|
|
809
809
|
}
|
|
@@ -818,7 +818,7 @@ class AuthClient {
|
|
|
818
818
|
/**
|
|
819
819
|
* Exchange a one-time OAuth ?code= for the session (plan §12).
|
|
820
820
|
* Web via /fenaura proxy: ALSO sets the first-party HttpOnly cookie.
|
|
821
|
-
* Non-web / backend: use `data.token`
|
|
821
|
+
* Non-web / backend: use `data.token` - store or forward it yourself
|
|
822
822
|
* (e.g. your backend sets it as a cookie for YOUR end users).
|
|
823
823
|
* Codes are single-use, 60s TTL; replay → INVALID_CODE.
|
|
824
824
|
* @param {string} code - one-time code from the callback redirect (?code=)
|
|
@@ -856,7 +856,7 @@ class AuthClient {
|
|
|
856
856
|
|
|
857
857
|
/**
|
|
858
858
|
* Send a password-reset code (account must exist; unknown addresses get a
|
|
859
|
-
* generic success without email
|
|
859
|
+
* generic success without email - anti-enumeration).
|
|
860
860
|
* @param {Object} params - { email }
|
|
861
861
|
* @returns {Promise<QueryResult>} { data: { code_sent, email }, error }
|
|
862
862
|
*/
|
|
@@ -876,7 +876,7 @@ class AuthClient {
|
|
|
876
876
|
}
|
|
877
877
|
|
|
878
878
|
/**
|
|
879
|
-
* Send a magic-link (OTP) code
|
|
879
|
+
* Send a magic-link (OTP) code - passwordless sign-in for existing accounts.
|
|
880
880
|
* @param {Object} params - { email }
|
|
881
881
|
* @returns {Promise<QueryResult>} { data: { code_sent, email }, error }
|
|
882
882
|
*/
|
|
@@ -1025,7 +1025,7 @@ class StorageClient {
|
|
|
1025
1025
|
* Upload a file in 1 MiB chunks, with live backend progress.
|
|
1026
1026
|
* Flow: init → PUT each chunk (server answers received/total per
|
|
1027
1027
|
* chunk) → complete (server assembles + sniffs content). A failed
|
|
1028
|
-
* chunk is retried alone
|
|
1028
|
+
* chunk is retried alone - never the whole file.
|
|
1029
1029
|
* @param {string} path - File path
|
|
1030
1030
|
* @param {File|Blob} file - File to upload
|
|
1031
1031
|
* @param {Object} [options] - Upload options
|
|
@@ -1045,7 +1045,7 @@ class StorageClient {
|
|
|
1045
1045
|
if (!isPathSafe(path)) {
|
|
1046
1046
|
throw new Error('Invalid path: directory traversal not allowed');
|
|
1047
1047
|
}
|
|
1048
|
-
// 0. compress
|
|
1048
|
+
// 0. compress - on-device; only the smaller bytes leave the device
|
|
1049
1049
|
let source = file;
|
|
1050
1050
|
let compression = null;
|
|
1051
1051
|
if (options.compress !== undefined && options.compress !== false && options.compress !== null) {
|
|
@@ -1067,7 +1067,7 @@ class StorageClient {
|
|
|
1067
1067
|
const creds = isProxy ? { credentials: 'include' } : {};
|
|
1068
1068
|
const fail = (code, message, request_id = '') => ({ status: 'error', data: null, error: { code, message }, request_id });
|
|
1069
1069
|
|
|
1070
|
-
// 1. init
|
|
1070
|
+
// 1. init - server validates name/type/size and hands back an upload id
|
|
1071
1071
|
const initRes = await fetch(`${this._url}/api/v1/blob/${this._apiKey}/init`, {
|
|
1072
1072
|
method: 'POST',
|
|
1073
1073
|
headers: { 'Content-Type': 'application/json', ...authHeaders },
|
|
@@ -1093,7 +1093,7 @@ class StorageClient {
|
|
|
1093
1093
|
}
|
|
1094
1094
|
};
|
|
1095
1095
|
|
|
1096
|
-
// 2. chunks
|
|
1096
|
+
// 2. chunks - sequential 1 MiB PUTs; each response is the progress event
|
|
1097
1097
|
for (let i = 0; i < chunks_total; i++) {
|
|
1098
1098
|
const start = i * chunk_size;
|
|
1099
1099
|
const end = Math.min(start + chunk_size, source.size);
|
|
@@ -1117,7 +1117,7 @@ class StorageClient {
|
|
|
1117
1117
|
}
|
|
1118
1118
|
}
|
|
1119
1119
|
|
|
1120
|
-
// 3. complete
|
|
1120
|
+
// 3. complete - server assembles, verifies size + content, stores
|
|
1121
1121
|
const doneRes = await fetch(`${this._url}/api/v1/blob/${this._apiKey}/complete/${upload_id}`, {
|
|
1122
1122
|
method: 'POST',
|
|
1123
1123
|
headers: { 'Content-Type': 'application/json', ...authHeaders },
|
|
@@ -1195,7 +1195,7 @@ class StorageClient {
|
|
|
1195
1195
|
},
|
|
1196
1196
|
|
|
1197
1197
|
/**
|
|
1198
|
-
* Same-origin session URL (B15: NOT public despite the name
|
|
1198
|
+
* Same-origin session URL (B15: NOT public despite the name - the GET
|
|
1199
1199
|
* still requires the caller's session cookie; outsiders get 401. For
|
|
1200
1200
|
* sharing with outsiders use createSignedUrl, which mints a real
|
|
1201
1201
|
* HMAC-signed expiring link server-side).
|
|
@@ -1265,10 +1265,10 @@ class StorageClient {
|
|
|
1265
1265
|
// Confirmed platform facts (researched 2026-09):
|
|
1266
1266
|
// - CompressionStream gzip/deflate: Baseline, all browsers since May 2023.
|
|
1267
1267
|
// - canvas.toBlob: PNG guaranteed; jpeg/webp widely supported BUT Safari may
|
|
1268
|
-
// silently fall back to PNG when webp encode is missing
|
|
1268
|
+
// silently fall back to PNG when webp encode is missing - so we NEVER trust
|
|
1269
1269
|
// the requested mime; the reported contentType is always out.type.
|
|
1270
1270
|
// - Video transcode (360p) needs WebCodecs + a muxer lib (WebCodecs emits raw
|
|
1271
|
-
// packets, not a playable file)
|
|
1271
|
+
// packets, not a playable file) - intentionally NOT in core; see Phase 2.
|
|
1272
1272
|
|
|
1273
1273
|
const COMPRESS_MODES = new Set(['auto', 'image', 'gzip', 'deflate', 'deflate-raw', 'none']);
|
|
1274
1274
|
const COMPRESS_IMAGE_FORMATS = new Set(['original', 'webp', 'jpeg', 'jpg', 'png']);
|
|
@@ -1356,7 +1356,7 @@ function requireBlob(file) {
|
|
|
1356
1356
|
// Image path: decode → scale-to-fit (aspect preserved, never upscale) →
|
|
1357
1357
|
// re-encode. Lossless (`lossless:true`) forces PNG. JPEG gets a white
|
|
1358
1358
|
// backdrop (no alpha channel). Anti-corruption: decode/encode failures throw
|
|
1359
|
-
// and the caller keeps the original
|
|
1359
|
+
// and the caller keeps the original - we never return partial bytes.
|
|
1360
1360
|
async function compressImageBlob(blob, o) {
|
|
1361
1361
|
if (!canCompressImage()) {
|
|
1362
1362
|
throw new Error('Image compression needs a browser (canvas/createImageBitmap unavailable)');
|
|
@@ -1366,7 +1366,7 @@ async function compressImageBlob(blob, o) {
|
|
|
1366
1366
|
try {
|
|
1367
1367
|
bitmap = await createImageBitmap(blob);
|
|
1368
1368
|
} catch {
|
|
1369
|
-
throw new Error('Could not decode image (unsupported or corrupt file
|
|
1369
|
+
throw new Error('Could not decode image (unsupported or corrupt file - original kept)');
|
|
1370
1370
|
}
|
|
1371
1371
|
try {
|
|
1372
1372
|
throwIfAborted(o.signal);
|
|
@@ -1377,7 +1377,7 @@ async function compressImageBlob(blob, o) {
|
|
|
1377
1377
|
canvas.width = w;
|
|
1378
1378
|
canvas.height = h;
|
|
1379
1379
|
const ctx = canvas.getContext('2d');
|
|
1380
|
-
if (!ctx) throw new Error('Canvas 2D unavailable
|
|
1380
|
+
if (!ctx) throw new Error('Canvas 2D unavailable - original kept');
|
|
1381
1381
|
let mime;
|
|
1382
1382
|
if (o.lossless) mime = 'image/png';
|
|
1383
1383
|
else if (o.format === 'original') mime = (blob.type && blob.type.startsWith('image/')) ? blob.type : 'image/jpeg';
|
|
@@ -1391,7 +1391,7 @@ async function compressImageBlob(blob, o) {
|
|
|
1391
1391
|
const q = (o.lossless || mime === 'image/png') ? undefined : o.quality;
|
|
1392
1392
|
const out = await new Promise((resolve, reject) => {
|
|
1393
1393
|
try {
|
|
1394
|
-
canvas.toBlob(b => (b ? resolve(b) : reject(new Error('Image encode failed
|
|
1394
|
+
canvas.toBlob(b => (b ? resolve(b) : reject(new Error('Image encode failed - original kept'))), mime, q);
|
|
1395
1395
|
} catch (err) {
|
|
1396
1396
|
reject(err);
|
|
1397
1397
|
}
|
|
@@ -1425,7 +1425,7 @@ async function compressGzipBlob(blob, format, signal) {
|
|
|
1425
1425
|
total += value.byteLength;
|
|
1426
1426
|
if (total > COMPRESS_MAX_BYTES) {
|
|
1427
1427
|
try { await reader.cancel(); } catch { /* noop */ }
|
|
1428
|
-
throw new Error('Compressed output exceeds 500 MB cap
|
|
1428
|
+
throw new Error('Compressed output exceeds 500 MB cap - original kept');
|
|
1429
1429
|
}
|
|
1430
1430
|
chunks.push(value);
|
|
1431
1431
|
}
|
|
@@ -1436,7 +1436,7 @@ async function compressGzipBlob(blob, format, signal) {
|
|
|
1436
1436
|
off += c.byteLength;
|
|
1437
1437
|
}
|
|
1438
1438
|
const out = new Blob([merged], { type: 'application/gzip' });
|
|
1439
|
-
if (!out.size) throw new Error('Compression produced empty output
|
|
1439
|
+
if (!out.size) throw new Error('Compression produced empty output - original kept');
|
|
1440
1440
|
return out;
|
|
1441
1441
|
}
|
|
1442
1442
|
|
|
@@ -1450,7 +1450,7 @@ async function compressGzipBlob(blob, format, signal) {
|
|
|
1450
1450
|
* preserved, never upscales), quality (0,1] default 0.8 (jpeg/webp only),
|
|
1451
1451
|
* format original|webp|jpeg|png, lossless:true forces PNG.
|
|
1452
1452
|
* - 'gzip'|'deflate'|'deflate-raw': lossless, any file type. Output is
|
|
1453
|
-
* `application/gzip`
|
|
1453
|
+
* `application/gzip` - decompress after download.
|
|
1454
1454
|
* - 'none': passthrough (no-op, still validates).
|
|
1455
1455
|
*
|
|
1456
1456
|
* Never corrupts: any decode/encode/stream failure throws and the original
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fenaura/sdk",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Fenaura Client SDK
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Fenaura Client SDK - direct database access, auth, and storage from the browser",
|
|
5
5
|
"main": "js/fenaura-client.js",
|
|
6
6
|
"types": "js/fenaura-client.d.ts",
|
|
7
7
|
"files": [
|