@riloservices/base 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rilo
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,256 @@
1
- # Temporary Holding Version
1
+ # @riloservices/base
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
+ Use a [Rilo DB](https://db.services.rilo.studio) Space from your server with one app key. The key works for your databases and for Files.
4
+
5
+ - **Databases.** `rilo.db('name')` gives you the MongoDB driver's own `Db`. Everything you know about the driver still applies.
6
+ - **Files.** Ask for an upload, send the file from the browser straight to storage, and make links to files.
7
+ - **Mongoose.** One call connects it with the same key.
8
+ - **A `rilodb://` link**, if you only need the database. See [below](#the-rilodb-link-databases-only).
9
+
10
+ Rilo DB works with MongoDB drivers and tools. MongoDB is a trademark of MongoDB, Inc.; Rilo DB is not affiliated with or endorsed by it.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @riloservices/base
16
+ ```
17
+
18
+ This also installs the official MongoDB driver (`mongodb`, version 6.10 or later): it's a peer dependency, so your app keeps one copy of it, and npm, pnpm and Bun add it for you. With Yarn, add it yourself: `yarn add @riloservices/base mongodb`.
19
+
20
+ It needs Node 20.19 or later, and works from ES modules and from CommonJS.
21
+
22
+ ## Your app key
23
+
24
+ Make an app key on the **App keys** page of your Space. It looks like this:
25
+
26
+ ```text
27
+ rilo_in_k3m9x2qz.app.<database secret>.<files secret>
28
+ ```
29
+
30
+ It is shown once, so copy it straight away. Put it in your server's environment as `RILO_KEY`:
31
+
32
+ ```bash
33
+ # .env (on your server, never in git)
34
+ RILO_KEY=rilo_in_k3m9x2qz.app.…
35
+ ```
36
+
37
+ **Keep `RILO_KEY` on your server.** Never put it in browser code, a mobile app or a public repository. The key can read and write your data, so treat it like a password. If it leaks, reset it on the App keys page.
38
+
39
+ `RILO_KEY` is the main way in, and the only value your app needs: it works for your databases and for Files.
40
+
41
+ ### Allow your app's address
42
+
43
+ Your Space only accepts database connections from its **Allowed places**. Before your app connects, add the address of the server it runs on (and your own, while you develop): open your Space, then **Settings**, then **Allowed places**. Until then, database calls fail to connect. Files doesn't use Allowed places, so it works from anywhere with the key.
44
+
45
+ ## The `rilodb://` link (databases only)
46
+
47
+ If a service only needs a database, give it a `rilodb://` link instead of the whole key. `rilo-db env` prints one from your key (see [below](#the-rilo-db-env-command)). It looks like this:
48
+
49
+ ```text
50
+ rilodb://app:<database secret>@k3m9x2qz.in.db.services.rilo.studio/salon-prod
51
+ ```
52
+
53
+ It holds the key's name, the database secret, the address and a database name, using the names you chose. Put it in the environment as `RILODB_URL` and pass it to the same functions:
54
+
55
+ ```js
56
+ const rilo = createClient(process.env.RILODB_URL);
57
+ const bookings = rilo.db().collection('bookings'); // the database the link names
58
+ ```
59
+
60
+ - **Only this package reads it.** MongoDB drivers and tools accept only `mongodb://` or `mongodb+srv://` links, so they can't use a `rilodb://` link. Give them the `mongodb://` link, which is `RILODB_URI` in the output of `rilo-db env`.
61
+ - **Databases only.** The link has no files part. Using `rilo.files` with it throws a `RiloKeyError`: Files needs your app key (`RILO_KEY`).
62
+ - **It brings its own address.** The `spacesDomain` and `port` options are ignored with a link. `tlsCAFile` and `appName` still apply.
63
+ - **The link's database is the default.** `rilo.db()` opens it. A `database` option or a name given to `rilo.db('name')` wins.
64
+ - **It holds a secret.** Treat `RILODB_URL` like `RILO_KEY`: keep it on your server, out of git and logs. If it leaks, reset the key on the App keys page.
65
+
66
+ ## Quick start
67
+
68
+ ```js
69
+ import { createClient } from '@riloservices/base';
70
+
71
+ const rilo = createClient(process.env.RILO_KEY);
72
+
73
+ // Databases: use the name you gave the database. The client adds your Space's id for you.
74
+ const bookings = rilo.db('salon-prod').collection('bookings');
75
+ await bookings.insertOne({ client: 'Aroha', at: new Date() });
76
+ console.log(await bookings.find({ client: 'Aroha' }).toArray());
77
+
78
+ // Close the connections when your app shuts down.
79
+ await rilo.close();
80
+ ```
81
+
82
+ `createClient` checks the key straight away and throws a [`RiloKeyError`](#errors) if it is missing or isn't an app key (or a `rilodb://` link). Nothing connects until you first call `rilo.db()`, and then one connection pool is opened and reused.
83
+
84
+ ### Upload a file from the browser
85
+
86
+ Files don't pass through your server or through Rilo. Your server asks for a signed upload, the browser sends the file straight to storage with it, and the upload carries no key.
87
+
88
+ On your server:
89
+
90
+ ```js
91
+ const photos = rilo.files.bucket('photos');
92
+
93
+ // For example in a route that your page calls before it uploads.
94
+ const ticket = await photos.createUpload({
95
+ path: 'avatars/user-42.jpg',
96
+ contentType: 'image/jpeg',
97
+ size: 204800, // the file's exact size in bytes
98
+ });
99
+ // Send `ticket` to the browser as JSON.
100
+ ```
101
+
102
+ In the browser:
103
+
104
+ ```js
105
+ import { sendUpload } from '@riloservices/base/browser';
106
+
107
+ await sendUpload(ticket, file); // `file` is a File or Blob
108
+ ```
109
+
110
+ `sendUpload` posts every signed field, then the file last, with no `Authorization` header. If storage refuses the file, it throws a `RiloUploadError` with storage's own status and reason. The upload link lasts 10 minutes. If a person cancels, stop the browser's request first (pass an `AbortSignal` as `{ signal }`), then call `photos.cancelUpload(ticket.uploadId)` on your server.
111
+
112
+ ### Links, lists and deletes
113
+
114
+ ```js
115
+ const photos = rilo.files.bucket('photos');
116
+
117
+ // A link to view a file. A private bucket's links expire (one hour unless you say).
118
+ const { url, expiresAt } = await photos.link('avatars/user-42.jpg', { expiresInSeconds: 900 });
119
+
120
+ // Up to 100 links at once, in the order you asked.
121
+ const links = await photos.links(['avatars/user-42.jpg', 'avatars/user-43.jpg']);
122
+
123
+ // One folder level at a time, a page at a time.
124
+ const page = await photos.list({ prefix: 'avatars/', delimiter: '/', limit: 100 });
125
+ // page.files, page.folders, page.nextCursor (pass it back as `cursor` for the next page)
126
+
127
+ const file = await photos.get('avatars/user-42.jpg'); // size, contentType, updatedAt
128
+ await photos.delete('avatars/user-42.jpg');
129
+
130
+ const { buckets, usage } = await rilo.files.buckets();
131
+ ```
132
+
133
+ Uploading and deleting need an app key with **Read and write** access. A **Read only** key can read data, list and get files, and make links.
134
+
135
+ Paths are sent to Files with each part encoded, so spaces and symbols in names are fine. The slashes between folders stay as they are.
136
+
137
+ ## Mongoose
138
+
139
+ Mongoose isn't a dependency of this package. Pass the one your app already has:
140
+
141
+ ```js
142
+ import mongoose from 'mongoose';
143
+ import { connectMongoose } from '@riloservices/base/mongoose';
144
+
145
+ await connectMongoose(mongoose, process.env.RILO_KEY, { database: 'salon-prod' });
146
+ ```
147
+
148
+ It connects with the same link that `createClient` uses, and opens `salon-prod` in your Space. It takes a `rilodb://` link in place of the key too; then `database` is optional, and the link's database is used when you leave it out.
149
+
150
+ ## Options
151
+
152
+ `createClient(keyOrLink, options)` and `connectMongoose(mongoose, keyOrLink, { database, ...options })` take these. Rilo's own addresses are the defaults, so you don't need any of them in production.
153
+
154
+ | Option | What it does |
155
+ | -------------- | --------------------------------------------------------------------------------------------------------------------------- |
156
+ | `database` | The database `rilo.db()` opens when you give it no name. (`connectMongoose` needs it, unless a `rilodb://` link names one.) |
157
+ | `appName` | A name for your app, shown in connection details so you can tell apps apart. |
158
+ | `spacesDomain` | The domain Spaces live under. Default `db.services.rilo.studio`. Ignored with a `rilodb://` link, which has its own. |
159
+ | `port` | The database port. Default `27017`. Ignored with a `rilodb://` link, which has its own. |
160
+ | `tlsCAFile` | A CA file to trust. Only for a local stand-in with its own certificate. |
161
+ | `filesUrl` | The Files address. Default `https://files.<region>.<spacesDomain>`, with the region from the key. |
162
+
163
+ ## The `rilo-db env` command
164
+
165
+ Some tools want the plain values instead of the key: Compass, mongosh, a language without this package, or Mongoose set up on its own. Drivers and tools need the `mongodb://` link, which is `RILODB_URI`. With `RILO_KEY` in your environment, or in a `.env` file in the current folder, run:
166
+
167
+ ```bash
168
+ npx @riloservices/base env
169
+ npx @riloservices/base env --database salon-prod
170
+ ```
171
+
172
+ It prints four lines:
173
+
174
+ ```text
175
+ RILODB_URI=mongodb://app_k3m9x2qz:…@k3m9x2qz.in.db.services.rilo.studio:27017/?tls=true&…
176
+ RILODB_URL=rilodb://app:…@k3m9x2qz.in.db.services.rilo.studio/
177
+ RILO_FILES_URL=https://files.in.db.services.rilo.studio
178
+ RILO_FILES_KEY=rdbf_k3m9x2qz_…
179
+ ```
180
+
181
+ - `RILODB_URI` is the `mongodb://` link, for MongoDB drivers and tools.
182
+ - `RILODB_URL` is the `rilodb://` link, for this package only.
183
+ - `RILO_FILES_URL` and `RILO_FILES_KEY` are for the Files API.
184
+
185
+ With `--database salon-prod`, both links name that database.
186
+
187
+ It runs on your machine and sends nothing anywhere. The output holds your secrets, so keep it out of chat, tickets, logs and git. If the key is missing or isn't an app key, it says so and exits with status 1.
188
+
189
+ ## Other languages
190
+
191
+ This package is a convenience for Node. Nothing in Rilo DB depends on it, and the key format is stable, so any language can use `RILO_KEY` with its own MongoDB driver and an HTTP client.
192
+
193
+ An app key has four parts, separated by dots:
194
+
195
+ ```text
196
+ rilo_<region>_<spaceId>.<name>.<databaseSecret>.<filesSecret>
197
+ ```
198
+
199
+ | | How to make it |
200
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
201
+ | Database login | `<name>_<spaceId>`, with `<databaseSecret>` as the password |
202
+ | Database address | `<spaceId>.<region>.db.services.rilo.studio`, port `27017` |
203
+ | Database name | `<your database>_<spaceId>` |
204
+ | Link options | `tls=true&directConnection=true&retryWrites=false&authSource=admin&maxPoolSize=10` |
205
+ | Files address | `https://files.<region>.db.services.rilo.studio` |
206
+ | Files key | `rdbf_<spaceId>_<filesSecret>`, sent as `Authorization: Bearer …`. **Never send the whole app key to Files:** it answers `app_key_sent`. |
207
+
208
+ For example, in Python:
209
+
210
+ ```python
211
+ import os
212
+ from pymongo import MongoClient
213
+
214
+ ident, name, db_secret, files_secret = os.environ["RILO_KEY"].split(".")
215
+ _, region, space = ident.split("_") # rilo_in_k3m9x2qz
216
+
217
+ client = MongoClient(
218
+ f"mongodb://{name}_{space}:{db_secret}@{space}.{region}.db.services.rilo.studio:27017/"
219
+ "?tls=true&directConnection=true&retryWrites=false&authSource=admin&maxPoolSize=10"
220
+ )
221
+ db = client[f"salon-prod_{space}"]
222
+ files_key = f"rdbf_{space}_{files_secret}" # the Bearer token for the Files API
223
+ ```
224
+
225
+ ## Errors
226
+
227
+ - **`RiloKeyError`** is thrown when the key or link is missing, isn't an app key or a `rilodb://` link, or when you use `rilo.files` with a `rilodb://` link. Its message says what is wrong, for example that you passed a files key, a `mongodb://` link or a broken `rilodb://` link, and never repeats what you gave it.
228
+ - **`RiloFilesError`** is thrown when a Files call fails. It has the HTTP `status`, a stable `code` such as `read_only_key` or `file_too_large`, a plain `message`, and where the Files API sends them, `details` (extra data, like `maxFileSize`) and `fields` (what to fix, on `validation_failed`). Decide what to do from `status` and `code`, not the message.
229
+ - **`RiloUploadError`** (from `@riloservices/base/browser`) is thrown when storage refuses an upload. It has the `status` and storage's own `code`, such as `AccessDenied` when the link has expired.
230
+ - **Database errors** are the MongoDB driver's own.
231
+
232
+ ```js
233
+ import { RiloFilesError } from '@riloservices/base';
234
+
235
+ try {
236
+ await rilo.files
237
+ .bucket('photos')
238
+ .createUpload({ path: 'a.jpg', contentType: 'image/jpeg', size });
239
+ } catch (error) {
240
+ if (error instanceof RiloFilesError && error.code === 'file_too_large') {
241
+ console.log(`The most allowed is ${String(error.details?.maxFileSize)} bytes.`);
242
+ } else {
243
+ throw error;
244
+ }
245
+ }
246
+ ```
247
+
248
+ Files calls aren't retried for you. Try again after a moment on a `429` or `503`, and give up after a few tries. Any other `4xx` gives the same answer when repeated, so fix the request instead.
249
+
250
+ ## What it never does
251
+
252
+ - It never logs anything, and never reads `process.env`. You pass the key in. (Only the `rilo-db` command reads `RILO_KEY`, as a command line tool.)
253
+ - It sends Files only the `rdbf_…` part of your key, in the `Authorization` header. The database secret is used only to open the database connection. A `rilodb://` link never reaches Files.
254
+ - It adds no network calls of its own: your data goes through the MongoDB driver, and files through the Files API and storage.
255
+
256
+ Rilo DB works with MongoDB drivers and tools. MongoDB is a trademark of MongoDB, Inc.; Rilo DB is not affiliated with or endorsed by it.
@@ -0,0 +1,39 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ // src/browser.ts
3
+ /** Storage refused the file. `code` and the message are storage's own, when it sent them. */
4
+ var RiloUploadError = class extends Error {
5
+ name = "RiloUploadError";
6
+ /** The HTTP status storage answered with. */
7
+ status;
8
+ /** Storage's error code, like `EntityTooLarge` or `AccessDenied`; null when it sent none. */
9
+ code;
10
+ constructor(status, code, reason) {
11
+ super(`Storage refused the upload (status ${status}${code === null ? "" : `, ${code}`})` + (reason === null ? "." : `: ${reason}`));
12
+ this.status = status;
13
+ this.code = code;
14
+ }
15
+ };
16
+ function xmlValue(xml, tag) {
17
+ const match = new RegExp(`<${tag}>([^<]*)</${tag}>`).exec(xml);
18
+ return match?.[1] === void 0 || match[1] === "" ? null : match[1];
19
+ }
20
+ /**
21
+ * Sends a file to storage with the upload your server asked for. Every signed field goes first,
22
+ * then the file, which must be last. There is no key here: the signed fields are the authority.
23
+ * Resolves when storage has the file; throws a `RiloUploadError` when it refuses it.
24
+ */
25
+ async function sendUpload(ticket, file, options = {}) {
26
+ const form = new FormData();
27
+ for (const [name, value] of Object.entries(ticket.fields)) form.append(name, value);
28
+ form.append("file", file);
29
+ const response = await fetch(ticket.url, {
30
+ method: "POST",
31
+ body: form,
32
+ signal: options.signal
33
+ });
34
+ if (response.ok) return;
35
+ const xml = await response.text().catch(() => "");
36
+ throw new RiloUploadError(response.status, xmlValue(xml, "Code"), xmlValue(xml, "Message"));
37
+ }
38
+ exports.RiloUploadError = RiloUploadError;
39
+ exports.sendUpload = sendUpload;
@@ -0,0 +1,21 @@
1
+ import { d as UploadTicket } from "./types-CD9NBWdA.cjs";
2
+ /** The two parts of an upload ticket that the browser needs. */
3
+ export type UploadForm = Pick<UploadTicket, 'url' | 'fields'>;
4
+ /** Storage refused the file. `code` and the message are storage's own, when it sent them. */
5
+ export declare class RiloUploadError extends Error {
6
+ readonly name = "RiloUploadError";
7
+ /** The HTTP status storage answered with. */
8
+ readonly status: number;
9
+ /** Storage's error code, like `EntityTooLarge` or `AccessDenied`; null when it sent none. */
10
+ readonly code: string | null;
11
+ constructor(status: number, code: string | null, reason: string | null);
12
+ }
13
+ /**
14
+ * Sends a file to storage with the upload your server asked for. Every signed field goes first,
15
+ * then the file, which must be last. There is no key here: the signed fields are the authority.
16
+ * Resolves when storage has the file; throws a `RiloUploadError` when it refuses it.
17
+ */
18
+ export declare function sendUpload(ticket: UploadForm, file: Blob, options?: {
19
+ signal?: AbortSignal;
20
+ }): Promise<void>;
21
+ export type { UploadTicket };
@@ -0,0 +1,21 @@
1
+ import { d as UploadTicket } from "./types-CD9NBWdA.mjs";
2
+ /** The two parts of an upload ticket that the browser needs. */
3
+ export type UploadForm = Pick<UploadTicket, 'url' | 'fields'>;
4
+ /** Storage refused the file. `code` and the message are storage's own, when it sent them. */
5
+ export declare class RiloUploadError extends Error {
6
+ readonly name = "RiloUploadError";
7
+ /** The HTTP status storage answered with. */
8
+ readonly status: number;
9
+ /** Storage's error code, like `EntityTooLarge` or `AccessDenied`; null when it sent none. */
10
+ readonly code: string | null;
11
+ constructor(status: number, code: string | null, reason: string | null);
12
+ }
13
+ /**
14
+ * Sends a file to storage with the upload your server asked for. Every signed field goes first,
15
+ * then the file, which must be last. There is no key here: the signed fields are the authority.
16
+ * Resolves when storage has the file; throws a `RiloUploadError` when it refuses it.
17
+ */
18
+ export declare function sendUpload(ticket: UploadForm, file: Blob, options?: {
19
+ signal?: AbortSignal;
20
+ }): Promise<void>;
21
+ export type { UploadTicket };
@@ -0,0 +1,37 @@
1
+ // src/browser.ts
2
+ /** Storage refused the file. `code` and the message are storage's own, when it sent them. */
3
+ var RiloUploadError = class extends Error {
4
+ name = "RiloUploadError";
5
+ /** The HTTP status storage answered with. */
6
+ status;
7
+ /** Storage's error code, like `EntityTooLarge` or `AccessDenied`; null when it sent none. */
8
+ code;
9
+ constructor(status, code, reason) {
10
+ super(`Storage refused the upload (status ${status}${code === null ? "" : `, ${code}`})` + (reason === null ? "." : `: ${reason}`));
11
+ this.status = status;
12
+ this.code = code;
13
+ }
14
+ };
15
+ function xmlValue(xml, tag) {
16
+ const match = new RegExp(`<${tag}>([^<]*)</${tag}>`).exec(xml);
17
+ return match?.[1] === void 0 || match[1] === "" ? null : match[1];
18
+ }
19
+ /**
20
+ * Sends a file to storage with the upload your server asked for. Every signed field goes first,
21
+ * then the file, which must be last. There is no key here: the signed fields are the authority.
22
+ * Resolves when storage has the file; throws a `RiloUploadError` when it refuses it.
23
+ */
24
+ async function sendUpload(ticket, file, options = {}) {
25
+ const form = new FormData();
26
+ for (const [name, value] of Object.entries(ticket.fields)) form.append(name, value);
27
+ form.append("file", file);
28
+ const response = await fetch(ticket.url, {
29
+ method: "POST",
30
+ body: form,
31
+ signal: options.signal
32
+ });
33
+ if (response.ok) return;
34
+ const xml = await response.text().catch(() => "");
35
+ throw new RiloUploadError(response.status, xmlValue(xml, "Code"), xmlValue(xml, "Message"));
36
+ }
37
+ export { RiloUploadError, sendUpload };