@fenaura/sdk 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agah enoch wesoamo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # @fenaura/sdk
2
+
3
+ Fenaura Client SDK — direct database access, auth, and storage from the browser. Zero dependencies. Works with any Fenaura BaaS backend.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @fenaura/sdk
9
+ ```
10
+
11
+ Requires Node >= 18 (browsers: modern evergreen with `fetch`).
12
+
13
+ ## Quickstart
14
+
15
+ First set up the same-domain proxy once per frontend (recommended for cookie auth):
16
+
17
+ ```bash
18
+ npx @fenaura/auth-init
19
+ ```
20
+
21
+ Then in your app:
22
+
23
+ ```js
24
+ import { createClient } from '@fenaura/sdk';
25
+
26
+ const fenaura = createClient('/fenaura', '<PROJECT_ID>');
27
+
28
+ await fenaura.auth.signIn({ email: 'jane@example.com', password: 'secret123' });
29
+ const { data, error } = await fenaura.from('todos').select('*').eq('done', false).limit(20);
30
+ ```
31
+
32
+ Get your `<PROJECT_ID>` (project slug) from the Fenaura dashboard.
33
+
34
+ ## Why the `/fenaura` proxy?
35
+
36
+ Fenaura auth uses `HttpOnly SameSite=Strict` session cookies. Calling the API cross-origin would drop cookies and hit CORS issues. The proxy maps same-origin `/fenaura/*` to your API, so auth just works in dev and prod with no code changes.
37
+
38
+ Direct (non-proxy) mode is for backends / Node scripts:
39
+
40
+ ```js
41
+ const fenaura = createClient('https://api.yours.com', '<PROJECT_ID>');
42
+ fenaura.setSession(token); // raw token from signIn(..., { browser: false })
43
+ ```
44
+
45
+ ## Database
46
+
47
+ ```js
48
+ // select
49
+ await fenaura.from('users').select('id,name').eq('role', 'admin').order('created_at').limit(10);
50
+ // insert
51
+ await fenaura.from('users').insert({ name: 'John' });
52
+ // update + delete are chained then awaited (Supabase-style)
53
+ await fenaura.from('users').update({ name: 'Jane' }).eq('id', 1).execute();
54
+ await fenaura.from('users').delete().eq('id', 1).execute();
55
+ // upsert takes a single object only
56
+ await fenaura.from('users').upsert({ id: 1, name: 'Jane' });
57
+ // single row
58
+ const user = await fenaura.from('users').select('*').eq('id', 1).single();
59
+ ```
60
+
61
+ Filters: `eq, neq, gt, gte, lt, lte, like, ilike, in, is, contains, containedBy, overlaps, textSearch, not, or`. Table/column names are validated client-side.
62
+
63
+ ## Auth
64
+
65
+ ```js
66
+ await fenaura.auth.signUp({ email, password });
67
+ await fenaura.auth.signIn({ email, password });
68
+ await fenaura.auth.signOut();
69
+ await fenaura.auth.getUser(); // current user via cookie or Bearer
70
+ await fenaura.auth.getSession();
71
+
72
+ // OAuth (12 providers: google, github, microsoft, apple, ...)
73
+ const { data } = await fenaura.auth.signInWithOAuth('google', { redirectTo: 'https://app.yours.com/auth/callback' });
74
+ // non-web / backend: { browser: false } returns { data: { url } }, then:
75
+ await fenaura.auth.exchangeCodeForSession(code);
76
+
77
+ // email verification / password reset / magic link
78
+ await fenaura.auth.sendVerificationCode({ email });
79
+ await fenaura.auth.submitVerificationCode({ email, code });
80
+ await fenaura.auth.sendPasswordReset({ email });
81
+ await fenaura.auth.confirmPasswordReset({ email, code, newPassword });
82
+ await fenaura.auth.sendMagicLink({ email });
83
+ await fenaura.auth.verifyMagicCode({ email, code });
84
+ ```
85
+
86
+ Pass `{ browser: false }` to `signUp/signIn/submitVerificationCode/confirmPasswordReset/verifyMagicCode` in non-browser contexts to get the raw token in `data.token` instead of a cookie.
87
+
88
+ ## Storage
89
+
90
+ Chunked (1 MiB), resumable per-chunk retry, with progress:
91
+
92
+ ```js
93
+ const { data, error } = await fenaura.storage.from('avatars').upload('user1.jpg', file, {
94
+ compress: true, // on-device image resize / gzip before upload
95
+ onProgress: ({ sentBytes, totalBytes }) => console.log(sentBytes / totalBytes),
96
+ });
97
+ const dl = await fenaura.storage.from('avatars').download('user1.jpg');
98
+ const url = fenaura.storage.from('avatars').getPublicUrl('user1.jpg'); // session-gated, needs cookie
99
+ const signed = await fenaura.storage.from('avatars').createSignedUrl('user1.jpg', 3600); // public HMAC link
100
+ ```
101
+
102
+ `compressFile(file, opts)` is also exported standalone. Images use canvas, other files use `CompressionStream`.
103
+
104
+ ## TypeScript
105
+
106
+ Types ship built-in:
107
+
108
+ ```ts
109
+ import { createClient, type FenauraClient } from '@fenaura/sdk';
110
+ ```
111
+
112
+ ## Bins
113
+
114
+ This package also ships the proxy installer:
115
+
116
+ ```bash
117
+ npx -p @fenaura/sdk fenaura-init
118
+ ```
119
+
120
+ Prefer the standalone `npx @fenaura/auth-init`.
121
+
122
+ ## License
123
+
124
+ MIT — see LICENSE.
package/bin/init.js ADDED
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+ // npx @fenaura/auth-init — run from your frontend folder.
3
+ // Writes the /fenaura/* proxy files your host needs. Idempotent.
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const argv = process.argv.slice(2);
8
+ const apiFlag = argv.find((a) => a.startsWith('--api='));
9
+ const API = apiFlag ? apiFlag.split('=')[1] : (process.env.FENAURA_API || 'http://localhost:8080');
10
+ const RULE_LINE = `/fenaura/* ${API}/:splat 200`;
11
+ const cwd = process.cwd();
12
+
13
+ // Outside an app folder? Ask the user to confirm cwd is their frontend root.
14
+ async function confirmRoot() {
15
+ if (fs.existsSync(path.join(cwd, 'package.json'))) return;
16
+ const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout });
17
+ const answer = await new Promise((resolve) => rl.question(`No package.json here. Is this your frontend root? (${cwd}) [Y/n] `, resolve));
18
+ rl.close();
19
+ if (!/^(y|yes)?\s*$/i.test(answer || '')) {
20
+ console.error('Aborted — cd into your frontend folder first, then re-run.');
21
+ process.exit(1);
22
+ }
23
+ }
24
+
25
+ async function main() {
26
+ await confirmRoot();
27
+ const report = [];
28
+ const has = (f) => fs.existsSync(path.join(cwd, f));
29
+ const hasRule = (f) => has(f) && fs.readFileSync(path.join(cwd, f), 'utf8').includes('/fenaura');
30
+
31
+ // 1. public/_redirects (Netlify + Cloudflare Pages read this)
32
+ const pubDir = path.join(cwd, 'public');
33
+ const redirectsFile = path.join(pubDir, '_redirects');
34
+ if (has('public/_redirects')) {
35
+ const cur = fs.readFileSync(redirectsFile, 'utf8');
36
+ const lines = cur.split('\n');
37
+ const idx = lines.findIndex(l => l.includes('/fenaura'));
38
+ if (idx !== -1) {
39
+ if (lines[idx].trim() === RULE_LINE) {
40
+ report.push('kept public/_redirects (rule already present)');
41
+ } else {
42
+ lines[idx] = RULE_LINE;
43
+ fs.writeFileSync(redirectsFile, lines.join('\n'));
44
+ report.push('fixed public/_redirects (corrected tampered rule)');
45
+ }
46
+ } else {
47
+ fs.appendFileSync(redirectsFile, (cur.length && !cur.endsWith('\n') ? '\n' : '') + RULE_LINE + '\n');
48
+ report.push('wrote public/_redirects');
49
+ }
50
+ } else {
51
+ fs.mkdirSync(pubDir, { recursive: true });
52
+ fs.writeFileSync(redirectsFile, RULE_LINE + '\n');
53
+ report.push('wrote public/_redirects');
54
+ }
55
+
56
+ // 2. vercel.json rewrites (Vercel only; ignored elsewhere)
57
+ const vercelPath = path.join(cwd, 'vercel.json');
58
+ const rewrite = { source: '/fenaura/:path*', destination: `${API}/:path*` };
59
+ let vercel = has('vercel.json') ? JSON.parse(fs.readFileSync(vercelPath, 'utf8')) : {};
60
+ vercel.rewrites = vercel.rewrites || [];
61
+ const existingIdx = vercel.rewrites.findIndex((r) => r.source && r.source.startsWith('/fenaura'));
62
+ if (existingIdx !== -1) {
63
+ const curDest = vercel.rewrites[existingIdx].destination;
64
+ if (curDest === rewrite.destination) {
65
+ report.push('kept vercel.json (rule already present)');
66
+ } else {
67
+ vercel.rewrites[existingIdx].destination = rewrite.destination;
68
+ fs.writeFileSync(vercelPath, JSON.stringify(vercel, null, 2) + '\n');
69
+ report.push('fixed vercel.json (corrected tampered destination)');
70
+ }
71
+ } else {
72
+ vercel.rewrites.push(rewrite);
73
+ fs.writeFileSync(vercelPath, JSON.stringify(vercel, null, 2) + '\n');
74
+ report.push('wrote vercel.json');
75
+ }
76
+
77
+ // 3. Dev proxy: write it directly (Vite / Next / SvelteKit / Astro / Nuxt detected by config file)
78
+ const PROXY_BLOCK = `'/fenaura': { target: '${API}', changeOrigin: true, rewrite: (p) => p.replace(/^\\/fenaura/, '') }`;
79
+ const viteCfg = fs.readdirSync(cwd).find((f) => /^vite\.config\./.test(f));
80
+ const nextCfg = fs.readdirSync(cwd).find((f) => /^next\.config\./.test(f));
81
+ // Additional vite-based frameworks fallback: SvelteKit, Astro, Nuxt all use vite under the hood
82
+ const svelteCfg = !viteCfg && fs.existsSync(path.join(cwd, 'svelte.config.js')) ? 'svelte.config.js' : null;
83
+ const astroCfg = !viteCfg && fs.existsSync(path.join(cwd, 'astro.config.mjs')) ? 'astro.config.mjs' : null;
84
+ const nuxtCfg = !viteCfg && fs.existsSync(path.join(cwd, 'nuxt.config.ts')) ? 'nuxt.config.ts' : null;
85
+ const fallbackVite = svelteCfg || astroCfg || nuxtCfg;
86
+ if (viteCfg) {
87
+ const vp = path.join(cwd, viteCfg);
88
+ let src = fs.readFileSync(vp, 'utf8');
89
+ if (src.includes('/fenaura')) {
90
+ // already has proxy — correct it if API mismatched (tampered)
91
+ if (src.includes(API)) {
92
+ report.push(`kept ${viteCfg} (proxy already present)`);
93
+ } else {
94
+ // replace target URL inside the fenaura block
95
+ src = src.replace(/(['"]\/fenaura['"]\s*:\s*\{\s*target:\s*['"])[^'"]+(['"])/, `$1${API}$2`);
96
+ // also handle json-style source destination case (next) not here
97
+ fs.writeFileSync(vp, src);
98
+ report.push(`fixed ${viteCfg} (corrected tampered API target → ${API})`);
99
+ }
100
+ } else if (/server\s*:\s*\{[^}]*proxy\s*:/.test(src)) {
101
+ // server.proxy exists — append our entry inside it
102
+ src = src.replace(/(proxy\s*:\s*\{)/, `$1\n ${PROXY_BLOCK},`);
103
+ fs.writeFileSync(vp, src);
104
+ report.push(`wrote ${viteCfg} (appended to server.proxy)`);
105
+ } else if (/defineConfig\(\s*\{/.test(src)) {
106
+ // no server key — add one
107
+ src = src.replace(/(defineConfig\(\s*\{)/, `$1\n server: {\n proxy: {\n ${PROXY_BLOCK},\n },\n },`);
108
+ fs.writeFileSync(vp, src);
109
+ report.push(`wrote ${viteCfg} (added server.proxy)`);
110
+ } else {
111
+ report.push(`${viteCfg} has no defineConfig({...) — add manually:`);
112
+ report.push(` ${PROXY_BLOCK}`);
113
+ }
114
+ } else if (nextCfg) {
115
+ report.push('next detected — rewrites() needs an async function; add manually:');
116
+ report.push(` { source: '/fenaura/:path*', destination: '${API}/:path*' }`);
117
+ } else if (fallbackVite) {
118
+ // SvelteKit / Astro / Nuxt: they all proxy via vite underneath, but config shape varies
119
+ // For these, ensure public/_redirects covers prod and print the vite snippet for dev
120
+ report.push(`${fallbackVite} detected (vite-based) — add to vite server.proxy for dev:`);
121
+ report.push(` ${PROXY_BLOCK}`);
122
+ report.push(' (public/_redirects already covers prod)');
123
+ } else {
124
+ // No framework config (plain HTML/CSS/JS): wire VS Code Live Server proxy.
125
+ const vsDir = path.join(cwd, '.vscode');
126
+ const vsPath = path.join(vsDir, 'settings.json');
127
+ let settings = {};
128
+ if (fs.existsSync(vsPath)) {
129
+ try { settings = JSON.parse(fs.readFileSync(vsPath, 'utf8')); } catch { settings = {}; }
130
+ }
131
+ const proxy = (settings['liveServer.settings.proxy'] = settings['liveServer.settings.proxy'] || {});
132
+ const expectedUri = 'http://localhost:3000/fenaura';
133
+ if (proxy.baseUri === '/fenaura' && proxy.proxyUri === expectedUri) {
134
+ report.push('kept .vscode/settings.json (Live Server proxy already present)');
135
+ } else if (proxy.baseUri === '/fenaura') {
136
+ proxy.enable = true;
137
+ proxy.baseUri = '/fenaura';
138
+ proxy.proxyUri = expectedUri;
139
+ fs.mkdirSync(vsDir, { recursive: true });
140
+ fs.writeFileSync(vsPath, JSON.stringify(settings, null, 2) + '\n');
141
+ report.push('fixed .vscode/settings.json (corrected tampered proxyUri)');
142
+ } else {
143
+ proxy.enable = true;
144
+ proxy.baseUri = '/fenaura';
145
+ // Via local nginx (:3000), which strips /fenaura — Live Server can't rewrite paths itself.
146
+ proxy.proxyUri = 'http://localhost:3000/fenaura';
147
+ fs.mkdirSync(vsDir, { recursive: true });
148
+ fs.writeFileSync(vsPath, JSON.stringify(settings, null, 2) + '\n');
149
+ report.push('wrote .vscode/settings.json (Live Server proxies /fenaura to dev API)');
150
+ }
151
+ }
152
+
153
+ const wroteAny = report.some(r => r.startsWith('wrote') || r.startsWith('fixed'));
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
+ console.log('\nFrontend calls (copy-paste, project ID from dashboard):');
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
+
160
+ // 4. Ensure JS intellisense for SDK in plain JS projects
161
+ const hasTsConfig = fs.existsSync(path.join(cwd, 'tsconfig.json'));
162
+ const hasJsConfig = fs.existsSync(path.join(cwd, 'jsconfig.json'));
163
+ if (!hasTsConfig && !hasJsConfig) {
164
+ const jsconfig = {
165
+ compilerOptions: {
166
+ target: "ES2020",
167
+ module: "ESNext",
168
+ moduleResolution: "bundler",
169
+ allowJs: true,
170
+ checkJs: false,
171
+ jsx: "react-jsx",
172
+ noImplicitAny: false
173
+ },
174
+ include: ["src"]
175
+ };
176
+ try {
177
+ fs.writeFileSync(path.join(cwd, 'jsconfig.json'), JSON.stringify(jsconfig, null, 2) + '\n');
178
+ report.push('wrote jsconfig.json (enables SDK intellisense for JS)');
179
+ } catch {}
180
+ }
181
+
182
+ // 5. Ensure SDK is installed if run via npx (one-command experience)
183
+ try {
184
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
185
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
186
+ const hasSdk = deps['@fenaura/client'] || deps['@fenaura/sdk'] || deps['fenaura'];
187
+ if (!hasSdk) {
188
+ console.log('\n → SDK not in package.json — installing @fenaura/sdk ...');
189
+ const { spawnSync } = require('child_process');
190
+ const res = spawnSync('npm', ['install', '@fenaura/sdk', '--save'], { stdio: 'inherit', cwd });
191
+ if (res.status !== 0) console.log(' (auto-install failed — run: npm install @fenaura/sdk)');
192
+ else console.log(' ✓ @fenaura/client installed');
193
+ }
194
+ } catch { /* no package.json or parse fail — skip auto-install */ }
195
+
196
+ // 6. Detect how the dev server is running + print next steps.
197
+ const net = require('net');
198
+ function portOpen(port) {
199
+ return new Promise((resolve) => {
200
+ const s = net.connect(port, '127.0.0.1');
201
+ s.on('connect', () => { s.end(); resolve(true); });
202
+ s.on('error', () => resolve(false));
203
+ setTimeout(() => { s.destroy(); resolve(false); }, 800);
204
+ });
205
+ }
206
+ (async () => {
207
+ const stack = viteCfg ? 'vite' : nextCfg ? 'next' : 'live-server';
208
+ const startCmd = stack === 'vite' || stack === 'next' ? 'npm run dev' : 'Go Live (VS Code status bar)';
209
+ const defaults = stack === 'vite' ? [5173] : stack === 'next' ? [3000] : [5500];
210
+ const open = [];
211
+ for (const p of [...defaults, 5173, 5174, 5199, 5500, 5501, 3000, 8080]) {
212
+ if (await portOpen(p)) open.push(p);
213
+ }
214
+ console.log('\nNext steps:');
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 — start the server first'})`);
217
+ console.log(' 3. Click login — expect session_minted + a fenaura_eusid_* cookie on YOUR domain (devtools → Application → Cookies).');
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 — no code changes.\n');
220
+ })();
221
+
222
+ }
223
+
224
+ main().catch((e) => { console.error(e.message); process.exit(1); });