@riloservices/base 0.0.0-stage → 0.1.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 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,250 @@
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 mongodb
16
+ ```
17
+
18
+ It needs Node 20.19 or later. `mongodb` (the official driver, version 6.10 or later) is a peer dependency, so your app keeps one copy of it. The package works from ES modules and from CommonJS.
19
+
20
+ ## Your app key
21
+
22
+ Make an app key on the **App keys** page of your Space. It looks like this:
23
+
24
+ ```text
25
+ rilo_in_k3m9x2qz.app.<database secret>.<files secret>
26
+ ```
27
+
28
+ It is shown once, so copy it straight away. Put it in your server's environment as `RILO_KEY`:
29
+
30
+ ```bash
31
+ # .env (on your server, never in git)
32
+ RILO_KEY=rilo_in_k3m9x2qz.app.…
33
+ ```
34
+
35
+ **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.
36
+
37
+ `RILO_KEY` is the main way in: it works for your databases and for Files.
38
+
39
+ ## The `rilodb://` link (databases only)
40
+
41
+ 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:
42
+
43
+ ```text
44
+ rilodb://app:<database secret>@k3m9x2qz.in.db.services.rilo.studio/salon-prod
45
+ ```
46
+
47
+ 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:
48
+
49
+ ```js
50
+ const rilo = createClient(process.env.RILODB_URL);
51
+ const bookings = rilo.db().collection('bookings'); // the database the link names
52
+ ```
53
+
54
+ - **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`.
55
+ - **Databases only.** The link has no files part. Using `rilo.files` with it throws a `RiloKeyError`: Files needs your app key (`RILO_KEY`).
56
+ - **It brings its own address.** The `spacesDomain` and `port` options are ignored with a link. `tlsCAFile` and `appName` still apply.
57
+ - **The link's database is the default.** `rilo.db()` opens it. A `database` option or a name given to `rilo.db('name')` wins.
58
+ - **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.
59
+
60
+ ## Quick start
61
+
62
+ ```js
63
+ import { createClient } from '@riloservices/base';
64
+
65
+ const rilo = createClient(process.env.RILO_KEY);
66
+
67
+ // Databases: use the name you gave the database. The client adds your Space's id for you.
68
+ const bookings = rilo.db('salon-prod').collection('bookings');
69
+ await bookings.insertOne({ client: 'Aroha', at: new Date() });
70
+ console.log(await bookings.find({ client: 'Aroha' }).toArray());
71
+
72
+ // Close the connections when your app shuts down.
73
+ await rilo.close();
74
+ ```
75
+
76
+ `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.
77
+
78
+ ### Upload a file from the browser
79
+
80
+ 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.
81
+
82
+ On your server:
83
+
84
+ ```js
85
+ const photos = rilo.files.bucket('photos');
86
+
87
+ // For example in a route that your page calls before it uploads.
88
+ const ticket = await photos.createUpload({
89
+ path: 'avatars/user-42.jpg',
90
+ contentType: 'image/jpeg',
91
+ size: 204800, // the file's exact size in bytes
92
+ });
93
+ // Send `ticket` to the browser as JSON.
94
+ ```
95
+
96
+ In the browser:
97
+
98
+ ```js
99
+ import { sendUpload } from '@riloservices/base/browser';
100
+
101
+ await sendUpload(ticket, file); // `file` is a File or Blob
102
+ ```
103
+
104
+ `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.
105
+
106
+ ### Links, lists and deletes
107
+
108
+ ```js
109
+ const photos = rilo.files.bucket('photos');
110
+
111
+ // A link to view a file. A private bucket's links expire (one hour unless you say).
112
+ const { url, expiresAt } = await photos.link('avatars/user-42.jpg', { expiresInSeconds: 900 });
113
+
114
+ // Up to 100 links at once, in the order you asked.
115
+ const links = await photos.links(['avatars/user-42.jpg', 'avatars/user-43.jpg']);
116
+
117
+ // One folder level at a time, a page at a time.
118
+ const page = await photos.list({ prefix: 'avatars/', delimiter: '/', limit: 100 });
119
+ // page.files, page.folders, page.nextCursor (pass it back as `cursor` for the next page)
120
+
121
+ const file = await photos.get('avatars/user-42.jpg'); // size, contentType, updatedAt
122
+ await photos.delete('avatars/user-42.jpg');
123
+
124
+ const { buckets, usage } = await rilo.files.buckets();
125
+ ```
126
+
127
+ 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.
128
+
129
+ 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.
130
+
131
+ ## Mongoose
132
+
133
+ Mongoose isn't a dependency of this package. Pass the one your app already has:
134
+
135
+ ```js
136
+ import mongoose from 'mongoose';
137
+ import { connectMongoose } from '@riloservices/base/mongoose';
138
+
139
+ await connectMongoose(mongoose, process.env.RILO_KEY, { database: 'salon-prod' });
140
+ ```
141
+
142
+ 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.
143
+
144
+ ## Options
145
+
146
+ `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.
147
+
148
+ | Option | What it does |
149
+ | -------------- | --------------------------------------------------------------------------------------------------------------------------- |
150
+ | `database` | The database `rilo.db()` opens when you give it no name. (`connectMongoose` needs it, unless a `rilodb://` link names one.) |
151
+ | `appName` | A name for your app, shown in connection details so you can tell apps apart. |
152
+ | `spacesDomain` | The domain Spaces live under. Default `db.services.rilo.studio`. Ignored with a `rilodb://` link, which has its own. |
153
+ | `port` | The database port. Default `27017`. Ignored with a `rilodb://` link, which has its own. |
154
+ | `tlsCAFile` | A CA file to trust. Only for a local stand-in with its own certificate. |
155
+ | `filesUrl` | The Files address. Default `https://files.<region>.<spacesDomain>`, with the region from the key. |
156
+
157
+ ## The `rilo-db env` command
158
+
159
+ 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:
160
+
161
+ ```bash
162
+ npx @riloservices/base env
163
+ npx @riloservices/base env --database salon-prod
164
+ ```
165
+
166
+ It prints four lines:
167
+
168
+ ```text
169
+ RILODB_URI=mongodb://app_k3m9x2qz:…@k3m9x2qz.in.db.services.rilo.studio:27017/?tls=true&…
170
+ RILODB_URL=rilodb://app:…@k3m9x2qz.in.db.services.rilo.studio/
171
+ RILO_FILES_URL=https://files.in.db.services.rilo.studio
172
+ RILO_FILES_KEY=rdbf_k3m9x2qz_…
173
+ ```
174
+
175
+ - `RILODB_URI` is the `mongodb://` link, for MongoDB drivers and tools.
176
+ - `RILODB_URL` is the `rilodb://` link, for this package only.
177
+ - `RILO_FILES_URL` and `RILO_FILES_KEY` are for the Files API.
178
+
179
+ With `--database salon-prod`, both links name that database.
180
+
181
+ 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.
182
+
183
+ ## Other languages
184
+
185
+ 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.
186
+
187
+ An app key has four parts, separated by dots:
188
+
189
+ ```text
190
+ rilo_<region>_<spaceId>.<name>.<databaseSecret>.<filesSecret>
191
+ ```
192
+
193
+ | | How to make it |
194
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
195
+ | Database login | `<name>_<spaceId>`, with `<databaseSecret>` as the password |
196
+ | Database address | `<spaceId>.<region>.db.services.rilo.studio`, port `27017` |
197
+ | Database name | `<your database>_<spaceId>` |
198
+ | Link options | `tls=true&directConnection=true&retryWrites=false&authSource=admin&maxPoolSize=10` |
199
+ | Files address | `https://files.<region>.db.services.rilo.studio` |
200
+ | Files key | `rdbf_<spaceId>_<filesSecret>`, sent as `Authorization: Bearer …`. **Never send the whole app key to Files:** it answers `app_key_sent`. |
201
+
202
+ For example, in Python:
203
+
204
+ ```python
205
+ import os
206
+ from pymongo import MongoClient
207
+
208
+ ident, name, db_secret, files_secret = os.environ["RILO_KEY"].split(".")
209
+ _, region, space = ident.split("_") # rilo_in_k3m9x2qz
210
+
211
+ client = MongoClient(
212
+ f"mongodb://{name}_{space}:{db_secret}@{space}.{region}.db.services.rilo.studio:27017/"
213
+ "?tls=true&directConnection=true&retryWrites=false&authSource=admin&maxPoolSize=10"
214
+ )
215
+ db = client[f"salon-prod_{space}"]
216
+ files_key = f"rdbf_{space}_{files_secret}" # the Bearer token for the Files API
217
+ ```
218
+
219
+ ## Errors
220
+
221
+ - **`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.
222
+ - **`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.
223
+ - **`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.
224
+ - **Database errors** are the MongoDB driver's own.
225
+
226
+ ```js
227
+ import { RiloFilesError } from '@riloservices/base';
228
+
229
+ try {
230
+ await rilo.files
231
+ .bucket('photos')
232
+ .createUpload({ path: 'a.jpg', contentType: 'image/jpeg', size });
233
+ } catch (error) {
234
+ if (error instanceof RiloFilesError && error.code === 'file_too_large') {
235
+ console.log(`The most allowed is ${String(error.details?.maxFileSize)} bytes.`);
236
+ } else {
237
+ throw error;
238
+ }
239
+ }
240
+ ```
241
+
242
+ 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.
243
+
244
+ ## What it never does
245
+
246
+ - 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.)
247
+ - 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.
248
+ - It adds no network calls of its own: your data goes through the MongoDB driver, and files through the Files API and storage.
249
+
250
+ 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 };