@nestcorehq/js 0.0.0-stage → 0.10.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 NestCore
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 CHANGED
@@ -1,3 +1,314 @@
1
- # Temporary Holding Version
1
+ # @nestcorehq/js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The official **browser/JavaScript SDK** for the [NestCore](https://nestcore.io)
4
+ platform — auth, database, a Supabase-style query builder, storage, realtime,
5
+ and edge functions, for web apps (React, Vue, Svelte, vanilla, …).
6
+
7
+ This is the client-side counterpart to [`@nestcorehq/node`](https://github.com/Orbitnest/nestcore_node):
8
+ the same API surface, packaged for the browser (ESM, CJS, and a CDN/IIFE bundle),
9
+ with session persistence and no Node-only dependencies.
10
+
11
+ ```bash
12
+ npm install @nestcorehq/js
13
+ ```
14
+
15
+ Or drop it in over a CDN — the global is `NestCore` (the old `OrbitNest` global
16
+ still works as a deprecated alias):
17
+
18
+ ```html
19
+ <script src="https://cdn.jsdelivr.net/npm/@nestcorehq/js"></script>
20
+ <script>
21
+ const nestcore = NestCore.createClient({ apiKey: 'YOUR_ANON_KEY' });
22
+ </script>
23
+ ```
24
+
25
+ > Use your project's **publishable anon key** in client-side code. Never ship a
26
+ > service-role key to the browser — it bypasses row-level security.
27
+
28
+ ---
29
+
30
+ ## Quick start
31
+
32
+ ```ts
33
+ import { createClient, localStorageAdapter } from '@nestcorehq/js';
34
+
35
+ const nestcore = createClient({
36
+ apiKey: import.meta.env.VITE_NESTCORE_ANON_KEY,
37
+ storage: localStorageAdapter, // keep the user signed in across page reloads
38
+ });
39
+
40
+ // Query with the fluent builder
41
+ const { data, error } = await nestcore.db
42
+ .from('posts')
43
+ .eq('published', true)
44
+ .order('created_at', { ascending: false })
45
+ .limit(10)
46
+ .select();
47
+
48
+ // Auth
49
+ await nestcore.auth.signIn({ email, password });
50
+ const user = nestcore.auth.getUser();
51
+
52
+ // Realtime
53
+ const channel = nestcore.channel('orders')
54
+ .on('postgres_changes', { event: 'INSERT', table: 'orders' }, (e) => console.log('new order', e.new))
55
+ .subscribe();
56
+ ```
57
+
58
+ The project slug is read from the API key's JWT, and `baseUrl` defaults to
59
+ `https://api.nestcore.io` — so `apiKey` is usually all you need. Pass `baseUrl`
60
+ for local dev.
61
+
62
+ ---
63
+
64
+ ## What's included
65
+
66
+ `createClient()` returns a client with these namespaces:
67
+
68
+ | Namespace | What it does |
69
+ | --- | --- |
70
+ | `db` | Raw SQL (`db.query`), table CRUD, RLS policy management, and the fluent query builder via `db.from(table)`. |
71
+ | `auth` | Email/password, email OTP, password recovery, profile, and **passkeys/WebAuthn**. Sessions persist via the configured storage adapter. |
72
+ | `storage` | `storage.from(bucket)` → `upload` / `download` / `list` / `remove` / `getPublicUrl`. |
73
+ | `realtime` / `channel()` | Postgres change subscriptions, broadcast, and presence over WebSocket. |
74
+ | `functions` | `functions.invoke(name, { body })` to call edge functions. |
75
+ | `jobs`, `env`, `logs` | Deprecated: these need a NestCore console admin token, so with a project key every call returns an `ADMIN_ONLY` error. Use the console, the CLI or the MCP server. |
76
+ | `analytics` | `identify` / `track` / `screen` / `crash`, with batched ingest. |
77
+
78
+ ### The query builder
79
+
80
+ `db.from(table)` returns a chainable, **parameterized** builder (every value is
81
+ bound, never interpolated) that mirrors `@nestcorehq/node` and Supabase:
82
+
83
+ ```ts
84
+ nestcore.db.from('users')
85
+ .eq('role', 'admin') // also: neq, gt, gte, lt, lte, like, ilike, in, is
86
+ .order('created_at', { ascending: false })
87
+ .range(0, 19) // or .limit(n) / .page(n, size)
88
+ .select('id, email, role'); // → { data: { rows, total }, error }
89
+
90
+ await nestcore.db.from('users').eq('id', id).single(); // exactly one row
91
+ await nestcore.db.from('users').eq('id', id).maybeSingle(); // one row or null
92
+
93
+ await nestcore.db.from('posts').insert({ title: 'Hi' });
94
+ await nestcore.db.from('posts').update(id, { title: 'Edited' });
95
+ await nestcore.db.from('posts').delete(id);
96
+ ```
97
+
98
+ Every call returns `{ data, error }` — check `error` before using `data` (the
99
+ SDK never throws on API errors).
100
+
101
+ ---
102
+
103
+ ## Session persistence
104
+
105
+ By default the session lives only in that client instance (safe for SSR: one
106
+ client per request never sees another request's user). Pass a storage adapter
107
+ to persist it:
108
+
109
+ ```ts
110
+ import { createClient, localStorageAdapter, createMemoryAdapter } from '@nestcorehq/js';
111
+
112
+ createClient({ apiKey, storage: localStorageAdapter }); // survives refresh
113
+ createClient({ apiKey, storage: createMemoryAdapter() }); // in-memory store you can share deliberately
114
+ ```
115
+
116
+ `localStorageAdapter` persists nothing when `localStorage` is unavailable (SSR,
117
+ private mode), so it's always safe to pass — on a server each client keeps its
118
+ own session.
119
+ You can also supply your own adapter — any object with
120
+ `getItem` / `setItem` / `removeItem`:
121
+
122
+ ```ts
123
+ import type { StorageAdapter } from '@nestcorehq/js';
124
+
125
+ const sessionStorageAdapter: StorageAdapter = {
126
+ getItem: (k) => sessionStorage.getItem(k),
127
+ setItem: (k, v) => sessionStorage.setItem(k, v),
128
+ removeItem: (k) => sessionStorage.removeItem(k),
129
+ };
130
+ ```
131
+
132
+ Persisted sessions are namespaced per project, so multiple clients on the same
133
+ origin don't collide.
134
+
135
+ ---
136
+
137
+ ### Calling auth from your server
138
+
139
+ Auth endpoints are throttled per client IP. When your server calls them for its users (a Next.js server action, an API route), every user would share your server's IP and its limit. Pass the user's IP with the **service-role key** and each user gets their own bucket; the same IP goes into the auth audit log:
140
+
141
+ ```ts
142
+ const client = createClient({
143
+ apiKey: process.env.NESTCORE_SERVICE_ROLE_KEY!, // never ship this key to a browser
144
+ clientIp: userIp, // e.g. the first address of x-forwarded-for set by YOUR proxy
145
+ });
146
+ await client.auth.signIn({ email, password });
147
+ ```
148
+
149
+ `clientIp` is sent as `X-NestCore-Client-IP` and must be a single IPv4 or IPv6 address. The API ignores it on requests made with the anon key or a user token, so a browser can't choose its own bucket. Create one client per request.
150
+
151
+ Project auth answers `429` with a `code` when a limit from the project's auth settings is reached: `OTP_RATE_LIMITED` (too many wrong codes), `OTP_REQUEST_LIMIT` (too many codes requested for one email in an hour), `ACCOUNT_LOCKED` (too many wrong passwords).
152
+
153
+ ## AI
154
+
155
+ Chat, streaming, tool calls and embeddings through the project's own provider keys (Console → Project Settings → AI). Calls need a signed-in user or the service-role key.
156
+
157
+ ```ts
158
+ const { data } = await client.ai.chat({ messages: [{ role: 'user', content: 'Summarise my last order' }] });
159
+
160
+ for await (const ev of client.ai.chatStream({ messages })) {
161
+ if (ev.type === 'delta') process.stdout.write(ev.text);
162
+ if (ev.type === 'error') console.error(ev.code, ev.message);
163
+ }
164
+
165
+ const { data: vectors } = await client.ai.embed(['first', 'second']); // 768 dimensions
166
+ ```
167
+
168
+ Tools: pass `tools: [{ name, description, parameters }]`; run each of `data.toolCalls`, then send the reply back as `{ role: 'assistant', content: null, toolCalls }` plus one `{ role: 'tool', toolCallId, content }` per call. See https://nestcore.io/docs/ai-gateway.
169
+
170
+ ### Memory, collections, threads, semantic search
171
+
172
+ ```ts
173
+ // Memory: facts about the signed-in user (near-duplicates update the same memory)
174
+ await client.memory.add('Prefers vegetarian recipes', { kind: 'preference' });
175
+ const { data: mem } = await client.memory.search('what should I cook?'); // mem.degraded = keyword fallback
176
+
177
+ // Collections: documents chunked, embedded and searched (hybrid vector + keyword)
178
+ await admin.collections.create({ name: 'handbook' }); // service-role key
179
+ await admin.collections.from('handbook').ingest({ url: 'https://example.com/guide.pdf' }, { wait: true });
180
+ const { data: hits } = await client.collections.from('handbook').search('How do refunds work?');
181
+
182
+ // Threads: saved conversations; replies stream and are stored as they arrive
183
+ const { data: thread } = await client.ai.threads.create({ title: 'Trip' });
184
+ for await (const ev of client.ai.threads.from(thread.id).run({ input: 'Plan a weekend', memory: true, collections: ['handbook'] })) {
185
+ if (ev.type === 'delta') process.stdout.write(ev.text);
186
+ if (ev.type === 'citation') console.log(`[${ev.index}]`, ev.title);
187
+ }
188
+
189
+ // Automatic embeddings (Console → AI → Embeddings), then search a table by meaning under RLS
190
+ const { data } = await client.db.from('articles').eq('published', true).semantic('body_embedding', 'reset my password', { k: 5 });
191
+ ```
192
+
193
+ See https://nestcore.io/docs/memory-api, /docs/collections, /docs/threads and /docs/embeddings.
194
+
195
+ #### Your own embeddings, trust and batches
196
+
197
+ Memory and collections also take vectors from your own (e.g. local) embedding model; the server then calls no provider and charges nothing. Search compares only vectors of the same `model` and size.
198
+
199
+ ```ts
200
+ const model = 'nomic-embed-text-v1.5';
201
+ await client.memory.add('Prefers tea', { embedding: vec, model, metadata: { trust: 'user' } });
202
+ await client.memory.search({ queryEmbedding: q, model, trust: ['user', 'tool'] }); // text query optional
203
+
204
+ // Batches: up to 100 adds, 20 searches, 500 ids / 20 subjects per call
205
+ await client.memory.addMany([{ content: 'a', embedding: va, model }, { content: 'b', embedding: vb, model }]);
206
+ const { data: batch } = await client.memory.searchMany([{ queryEmbedding: q1, model }, { query: 'tea' }]);
207
+ await client.memory.forgetMany({ ids: ['…'], subjects: ['owner:…'] });
208
+
209
+ // A collection document from chunks you embedded (ready at once; the first chunks fix the collection's model)
210
+ await client.collections.from('notes').addChunks([{ content: 'Refunds take 5 days', embedding: v }], { model, title: 'FAQ' });
211
+ await client.collections.from('notes').search({ queryEmbedding: q, model, trust: 'user' });
212
+ ```
213
+
214
+ ### AI credits and the OpenAI-compatible API
215
+
216
+ A project's own provider key always wins. Without one, the workspace's prepaid AI credit can pay (bought in Console → Settings → Billing).
217
+
218
+ ```ts
219
+ const { data: credits } = await client.ai.credits();
220
+ // { available, active, balanceUsd, monthlyCapUsd, spentThisMonthUsd, ownKeys, creditProviders }
221
+
222
+ try {
223
+ unwrap(await client.ai.chat({ messages }));
224
+ } catch (e) {
225
+ if (e instanceof NestCoreError && e.isCreditsExhausted) {
226
+ // e.reason: 'balance' | 'spend_cap'. Don't retry: offer e.buyUrl, or adding your own key.
227
+ }
228
+ }
229
+
230
+ // Any OpenAI client works against the gateway (anon key as `apikey`, the user's token as the API key)
231
+ new OpenAI({ baseURL: client.ai.openaiBaseUrl, apiKey: accessToken, defaultHeaders: { apikey: anonKey } });
232
+ ```
233
+
234
+ ## Realtime
235
+
236
+ Browsers can't set WebSocket request headers, so the API key is sent via the
237
+ connection subprotocol (never in the URL — it would leak into logs/history). In
238
+ the browser the SDK uses the native `WebSocket`; for isomorphic/SSR use on
239
+ Node.js < 22, pass one:
240
+
241
+ ```ts
242
+ import WebSocket from 'ws';
243
+ createClient({ apiKey, realtimeOptions: { WebSocket } });
244
+ ```
245
+
246
+ ```ts
247
+ const channel = nestcore.channel('room-1')
248
+ .on('broadcast', { event: 'cursor' }, (msg) => render(msg.payload))
249
+ .on('postgres_changes', { event: '*', table: 'messages' }, (e) => append(e.new))
250
+ .subscribe((status) => console.log(status));
251
+
252
+ channel.send('cursor', { x, y });
253
+ channel.presence.track({ user: 'me' });
254
+ ```
255
+
256
+ ---
257
+
258
+ ## TypeScript types from your schema
259
+
260
+ Generate row types from the live schema for end-to-end safety (typically run
261
+ server-side / in a build step, since it needs a service-role key):
262
+
263
+ ```ts
264
+ import { generateTypes } from '@nestcorehq/js';
265
+ const ts = await generateTypes(serverClient.db); // → string of TS interfaces + a `Database` map
266
+ ```
267
+
268
+ > Prefer the [`nestcore` CLI](https://github.com/Orbitnest/nestcore_cli)'s
269
+ > `nestcore gen types` for this in projects.
270
+
271
+ ---
272
+
273
+ ## Browser support
274
+
275
+ Targets ES2020 and any environment with `fetch`, `WebSocket`, `FormData`, and
276
+ `Blob` (all modern browsers; Node 22+ for isomorphic use). No polyfills needed.
277
+
278
+ `@nestcorehq/node` vs `@nestcorehq/js`: the Node SDK additionally ships a
279
+ filesystem-backed **migration runner**; the web SDK omits it (no filesystem in
280
+ the browser) and adds **session persistence adapters**. Everything else is the
281
+ same API.
282
+
283
+ ---
284
+
285
+ ## Development
286
+
287
+ ```bash
288
+ npm install
289
+ npm run build # → dist/ : ESM (.mjs), CJS (.js), IIFE (.global.js), + .d.ts
290
+ npm run typecheck
291
+ ```
292
+
293
+ ---
294
+
295
+ ## Migrating from `@orbitneststudio/js`
296
+
297
+ The SDK was renamed from OrbitNest to NestCore. Switch the dependency to
298
+ `@nestcorehq/js` and update your imports; nothing else has to change at once:
299
+
300
+ - The old export names still work as deprecated aliases: `OrbitNestError`
301
+ (same class as `NestCoreError`, so `instanceof` checks keep working),
302
+ `OrbitNestClient` and `OrbitNestConfig`.
303
+ - The default `baseUrl` is now `https://api.nestcore.io`.
304
+ - `clientIp` is now sent as `X-NestCore-Client-IP`.
305
+ - Sessions saved under the old `orbitnest.session.*` storage key and the
306
+ analytics ids under `orbitnest_*` are moved to the new `nestcore` keys on
307
+ first read, so signed-in users stay signed in.
308
+ - The CDN global `OrbitNest` is kept as an alias of `NestCore`.
309
+
310
+ ---
311
+
312
+ ## License
313
+
314
+ MIT © NestCore