@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 +21 -0
- package/README.md +249 -2
- package/dist/browser.cjs +39 -0
- package/dist/browser.d.cts +21 -0
- package/dist/browser.d.mts +21 -0
- package/dist/browser.mjs +37 -0
- package/dist/cli.mjs +307 -0
- package/dist/errors-DwVg5fNj.d.cts +24 -0
- package/dist/errors-DwVg5fNj.d.mts +24 -0
- package/dist/index.cjs +124 -0
- package/dist/index.d.cts +57 -0
- package/dist/index.d.mts +57 -0
- package/dist/index.mjs +121 -0
- package/dist/key-B7koHuaE.mjs +272 -0
- package/dist/key-DPXUxbGX.cjs +289 -0
- package/dist/mongoose.cjs +21 -0
- package/dist/mongoose.d.cts +29 -0
- package/dist/mongoose.d.mts +29 -0
- package/dist/mongoose.mjs +19 -0
- package/dist/types-CD9NBWdA.d.cts +88 -0
- package/dist/types-CD9NBWdA.d.mts +88 -0
- package/package.json +72 -4
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
// ../shared/src/connect.ts
|
|
5
|
+
/**
|
|
6
|
+
* The fixed options of every link, in this order. `directConnection` keeps the driver talking only
|
|
7
|
+
* to the Gateway; DocumentDB has no retryable writes; logins live in `admin`; `maxPoolSize=10`
|
|
8
|
+
* fits 4 app instances in the Seed limit of 50, with 2 monitoring connections each (decision Q3).
|
|
9
|
+
*/
|
|
10
|
+
const CONNECT_OPTIONS = {
|
|
11
|
+
tls: true,
|
|
12
|
+
directConnection: true,
|
|
13
|
+
retryWrites: false,
|
|
14
|
+
authSource: "admin",
|
|
15
|
+
maxPoolSize: 10
|
|
16
|
+
};
|
|
17
|
+
// ../shared/src/logins.ts
|
|
18
|
+
/**
|
|
19
|
+
* The rule for a password the person chooses (step 11): 16 to 64 letters, digits, `-` and `_`.
|
|
20
|
+
* These are the characters a `mongodb://` link carries without encoding. Rilo's own suggestion is
|
|
21
|
+
* 24 random bytes in base64url (32 characters), which always passes.
|
|
22
|
+
*/
|
|
23
|
+
const LOGIN_PASSWORD = {
|
|
24
|
+
min: 16,
|
|
25
|
+
max: 64,
|
|
26
|
+
pattern: /^[A-Za-z0-9_-]+$/
|
|
27
|
+
};
|
|
28
|
+
// ../shared/src/regions.ts
|
|
29
|
+
const REGION_CODES = ["in", "au"];
|
|
30
|
+
const SPACE_ID_PATTERN = new RegExp(`^[bcdfghjkmnpqrstvwxz23456789]{8}$`);
|
|
31
|
+
function isSpaceId(value) {
|
|
32
|
+
return SPACE_ID_PATTERN.test(value);
|
|
33
|
+
}
|
|
34
|
+
// ../shared/src/app-keys.ts
|
|
35
|
+
/** Every app key starts with this, so people, scanners and the Files service recognise one. */
|
|
36
|
+
const APP_KEY_PREFIX = "rilo_";
|
|
37
|
+
/** The environment variable an app keeps its key in. */
|
|
38
|
+
const APP_KEY_ENV_VAR = "RILO_KEY";
|
|
39
|
+
/**
|
|
40
|
+
* An app key's name is also its login's and its files key's name, so it follows both rules:
|
|
41
|
+
* lowercase letters, digits and single hyphens, starting with a letter, 3 to 32 characters.
|
|
42
|
+
*/
|
|
43
|
+
const APP_KEY_NAME = {
|
|
44
|
+
min: 3,
|
|
45
|
+
max: 32,
|
|
46
|
+
pattern: /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
|
|
47
|
+
};
|
|
48
|
+
/** The files secret: 32 random bytes in base64url, 43 characters. */
|
|
49
|
+
const FILES_SECRET = /^[A-Za-z0-9_-]{43}$/;
|
|
50
|
+
const IDENTITY = /^rilo_([a-z]+)_([a-z0-9]+)$/;
|
|
51
|
+
/** What is wrong with an app key name, in plain words, or null. Nothing is trimmed or lowered. */
|
|
52
|
+
function appKeyNameProblem(name) {
|
|
53
|
+
if (name === "") return "Enter a name for the key.";
|
|
54
|
+
if (name.trim() !== name) return "Remove the spaces at the start or end of the name.";
|
|
55
|
+
if (/\s/.test(name)) return "Use hyphens instead of spaces, like web-app.";
|
|
56
|
+
if (/[A-Z]/.test(name)) return "Use lowercase letters only, like web-app.";
|
|
57
|
+
if (!/^[a-z0-9-]+$/.test(name)) return "Use only lowercase letters, digits and hyphens.";
|
|
58
|
+
if (!/^[a-z]/.test(name)) return "Start the name with a letter.";
|
|
59
|
+
if (name.includes("--")) return "Use single hyphens, never two in a row.";
|
|
60
|
+
if (name.endsWith("-")) return "End the name with a letter or digit, not a hyphen.";
|
|
61
|
+
if (name.length < APP_KEY_NAME.min) return `Use at least ${APP_KEY_NAME.min} characters.`;
|
|
62
|
+
if (name.length > APP_KEY_NAME.max) return `Use ${APP_KEY_NAME.max} characters or fewer.`;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function isDatabaseSecret(value) {
|
|
66
|
+
return LOGIN_PASSWORD.pattern.test(value) && value.length >= LOGIN_PASSWORD.min && value.length <= LOGIN_PASSWORD.max;
|
|
67
|
+
}
|
|
68
|
+
function isRegion(value) {
|
|
69
|
+
return REGION_CODES.includes(value);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The parts of an app key, or null when the text isn't exactly one. Strict: anything else, even
|
|
73
|
+
* with the right prefix, is refused. It says nothing about whether the key is real.
|
|
74
|
+
*/
|
|
75
|
+
function parseAppKey(key) {
|
|
76
|
+
if (!key.startsWith("rilo_")) return null;
|
|
77
|
+
const pieces = key.split(".");
|
|
78
|
+
if (pieces.length !== 4) return null;
|
|
79
|
+
const [identity = "", name = "", databaseSecret = "", filesSecret = ""] = pieces;
|
|
80
|
+
const match = IDENTITY.exec(identity);
|
|
81
|
+
const region = match?.[1];
|
|
82
|
+
const spaceId = match?.[2];
|
|
83
|
+
if (region === void 0 || spaceId === void 0) return null;
|
|
84
|
+
if (!isRegion(region) || !isSpaceId(spaceId)) return null;
|
|
85
|
+
if (appKeyNameProblem(name) !== null) return null;
|
|
86
|
+
if (!isDatabaseSecret(databaseSecret) || !FILES_SECRET.test(filesSecret)) return null;
|
|
87
|
+
return {
|
|
88
|
+
region,
|
|
89
|
+
spaceId,
|
|
90
|
+
name,
|
|
91
|
+
databaseSecret,
|
|
92
|
+
filesSecret
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** The `mongodb://` link MongoDB drivers and tools need, with the password in it. */
|
|
96
|
+
function mongoLink(link) {
|
|
97
|
+
const options = Object.entries(CONNECT_OPTIONS).map(([key, value]) => `${key}=${value}`);
|
|
98
|
+
if (link.tlsCAFile) options.push(`tlsCAFile=${encodeURIComponent(link.tlsCAFile)}`);
|
|
99
|
+
const path = link.databaseRealName ? encodeURIComponent(link.databaseRealName) : "";
|
|
100
|
+
return `mongodb://${`${encodeURIComponent(link.loginRealName)}:${encodeURIComponent(link.password)}`}@${link.host}:${link.port}/${path}?${options.join("&")}`;
|
|
101
|
+
}
|
|
102
|
+
/** Everything an app needs from its key: the same values the Connect card shows. */
|
|
103
|
+
function appKeyValues(parts, addresses, database) {
|
|
104
|
+
const loginRealName = `${parts.name}_${parts.spaceId}`;
|
|
105
|
+
const host = `${parts.spaceId}.${parts.region}.${addresses.spacesDomain}`;
|
|
106
|
+
return {
|
|
107
|
+
loginRealName,
|
|
108
|
+
filesKey: `rdbf_${parts.spaceId}_${parts.filesSecret}`,
|
|
109
|
+
host,
|
|
110
|
+
filesUrl: addresses.filesUrl,
|
|
111
|
+
connectLink: mongoLink({
|
|
112
|
+
loginRealName,
|
|
113
|
+
password: parts.databaseSecret,
|
|
114
|
+
host,
|
|
115
|
+
port: addresses.port,
|
|
116
|
+
databaseRealName: database ? `${database}_${parts.spaceId}` : null,
|
|
117
|
+
tlsCAFile: addresses.tlsCAFile
|
|
118
|
+
}),
|
|
119
|
+
rilodbLink: formatRilodbLink(parts, addresses, database)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** The environment variable for a `rilodb://` link. */
|
|
123
|
+
const RILODB_LINK_ENV_VAR = "RILODB_URL";
|
|
124
|
+
/** The `rilodb://` link of a key, naming `database` when one is given. */
|
|
125
|
+
function formatRilodbLink(parts, addresses, database) {
|
|
126
|
+
const port = addresses.port === 27017 ? "" : `:${addresses.port}`;
|
|
127
|
+
const host = `${parts.spaceId}.${parts.region}.${addresses.spacesDomain}${port}`;
|
|
128
|
+
const path = database ? encodeURIComponent(database) : "";
|
|
129
|
+
return `rilodb://${parts.name}:${parts.databaseSecret}@${host}/${path}`;
|
|
130
|
+
}
|
|
131
|
+
// src/errors.ts
|
|
132
|
+
/** The key or link is missing or isn't an app key or a `rilodb://` link. The message never repeats it. */
|
|
133
|
+
var RiloKeyError = class extends Error {
|
|
134
|
+
name = "RiloKeyError";
|
|
135
|
+
};
|
|
136
|
+
const MISSING = `No app key was given. Set ${APP_KEY_ENV_VAR} to your app key from the App keys page of your Space, on your server, and pass it in.`;
|
|
137
|
+
const MISSING_EITHER = `${MISSING} A rilodb:// link works too, for databases only.`;
|
|
138
|
+
const FILES_KEY = `That is a files key (it starts with rdbf_), not an app key. Use the app key, which starts with ${APP_KEY_PREFIX}. It works for databases and Files.`;
|
|
139
|
+
const MONGO_LINK = `That is a mongodb:// connect link, not an app key. Use the app key, which starts with ${APP_KEY_PREFIX}. It works for databases and Files. MongoDB drivers and tools use the mongodb:// link; this client reads the app key.`;
|
|
140
|
+
const MONGO_LINK_EITHER = `That is a mongodb:// connect link, not an app key or a rilodb:// link. Use the app key, which starts with ${APP_KEY_PREFIX}, or the rilodb:// link. MongoDB drivers and tools use the mongodb:// link; this client reads the app key or the rilodb:// link.`;
|
|
141
|
+
const RILODB_LINK = `That is a rilodb:// link, not an app key. This needs the app key (${APP_KEY_ENV_VAR}), which starts with ${APP_KEY_PREFIX}. A rilodb:// link holds only the database part.`;
|
|
142
|
+
const MALFORMED = `That is not a valid app key. An app key starts with ${APP_KEY_PREFIX} and has four parts separated by dots. Copy the whole key again from the App keys page. A lost key can't be shown again, so reset it there.`;
|
|
143
|
+
const MALFORMED_EITHER = `That is not a valid app key or rilodb:// link. An app key starts with ${APP_KEY_PREFIX} and has four parts separated by dots. Copy the whole key again from the App keys page. A lost key can't be shown again, so reset it there.`;
|
|
144
|
+
const isRilodbLink = (text) => /^rilodb:\/\//i.test(text);
|
|
145
|
+
const isMongoLink = (text) => /^mongodb(\+srv)?:\/\//i.test(text);
|
|
146
|
+
/**
|
|
147
|
+
* The text of a key, trimmed, or a `RiloKeyError` that says what was given instead. `orLink` words
|
|
148
|
+
* the refusals for a place that takes a `rilodb://` link too. No message repeats any of the input.
|
|
149
|
+
*/
|
|
150
|
+
function keyText(key, orLink) {
|
|
151
|
+
if (key === void 0 || key === null || typeof key === "string" && key.trim() === "") throw new RiloKeyError(orLink ? MISSING_EITHER : MISSING);
|
|
152
|
+
if (typeof key !== "string") throw new RiloKeyError(orLink ? MALFORMED_EITHER : MALFORMED);
|
|
153
|
+
const text = key.trim();
|
|
154
|
+
if (text.startsWith("rdbf_")) throw new RiloKeyError(FILES_KEY);
|
|
155
|
+
if (isMongoLink(text)) throw new RiloKeyError(orLink ? MONGO_LINK_EITHER : MONGO_LINK);
|
|
156
|
+
if (isRilodbLink(text)) throw new RiloKeyError(RILODB_LINK);
|
|
157
|
+
return text;
|
|
158
|
+
}
|
|
159
|
+
function resolveAppKey(text, orLink, options, database) {
|
|
160
|
+
const parts = parseAppKey(text);
|
|
161
|
+
if (parts === null) throw new RiloKeyError(orLink ? MALFORMED_EITHER : MALFORMED);
|
|
162
|
+
const spacesDomain = options.spacesDomain ?? "db.services.rilo.studio";
|
|
163
|
+
const filesUrl = (options.filesUrl ?? `https://files.${parts.region}.${spacesDomain}`).replace(/\/+$/, "");
|
|
164
|
+
return {
|
|
165
|
+
parts,
|
|
166
|
+
values: appKeyValues(parts, {
|
|
167
|
+
spacesDomain,
|
|
168
|
+
port: options.port ?? 27017,
|
|
169
|
+
filesUrl,
|
|
170
|
+
tlsCAFile: options.tlsCAFile
|
|
171
|
+
}, database)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The app key's parts and the values built from them, or a `RiloKeyError`. The error says what is
|
|
176
|
+
* wrong and never contains any part of the key. A `rilodb://` link is refused: this is for what
|
|
177
|
+
* needs the whole key.
|
|
178
|
+
*/
|
|
179
|
+
function resolveKey(key, options = {}, database) {
|
|
180
|
+
return resolveAppKey(keyText(key, false), false, options, database);
|
|
181
|
+
}
|
|
182
|
+
// src/env-command.ts
|
|
183
|
+
const KEY_VAR = "RILO_KEY";
|
|
184
|
+
const HELP = `rilo-db: tools for your Rilo DB app key
|
|
185
|
+
|
|
186
|
+
Usage:
|
|
187
|
+
rilo-db env [--database <name>] Print RILODB_URI, RILODB_URL, RILO_FILES_URL and RILO_FILES_KEY
|
|
188
|
+
rilo-db --help Show this help
|
|
189
|
+
|
|
190
|
+
Use "env" when a tool or language needs the plain values: Compass, mongosh, Python, Go or Mongoose
|
|
191
|
+
set up on its own. It reads ${KEY_VAR} from the environment, or from a .env file in this folder. It
|
|
192
|
+
runs on your machine and sends nothing anywhere.
|
|
193
|
+
|
|
194
|
+
What it prints:
|
|
195
|
+
RILODB_URI The mongodb:// link, for MongoDB drivers and tools
|
|
196
|
+
RILODB_URL The rilodb:// link, for the Rilo client (databases only; drivers can't read it)
|
|
197
|
+
RILO_FILES_URL The Files address
|
|
198
|
+
RILO_FILES_KEY The files key
|
|
199
|
+
|
|
200
|
+
The output holds your secrets. Keep it out of chat, tickets, logs and git.
|
|
201
|
+
|
|
202
|
+
Options:
|
|
203
|
+
--database <name> Put this database in both links, by the name you gave it, like salon-prod
|
|
204
|
+
-h, --help Show this help
|
|
205
|
+
`;
|
|
206
|
+
/**
|
|
207
|
+
* The value of `name` in the text of a `.env` file, or undefined. Handles `export`, comments, quotes
|
|
208
|
+
* and Windows line endings; it expands nothing. The last line that sets it wins.
|
|
209
|
+
*/
|
|
210
|
+
function readDotenvValue(text, name) {
|
|
211
|
+
const body = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
212
|
+
let found;
|
|
213
|
+
for (const raw of body.split(/\r?\n/)) {
|
|
214
|
+
const line = raw.trim().replace(/^export\s+/, "");
|
|
215
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
216
|
+
const equals = line.indexOf("=");
|
|
217
|
+
if (equals === -1 || line.slice(0, equals).trim() !== name) continue;
|
|
218
|
+
const value = line.slice(equals + 1).trim();
|
|
219
|
+
const quote = value[0];
|
|
220
|
+
if ((quote === "\"" || quote === "'") && value.indexOf(quote, 1) > 0) found = value.slice(1, value.indexOf(quote, 1));
|
|
221
|
+
else found = value.replace(/\s+#.*$/, "");
|
|
222
|
+
}
|
|
223
|
+
return found;
|
|
224
|
+
}
|
|
225
|
+
function readKey(context) {
|
|
226
|
+
const fromEnv = context.env[KEY_VAR];
|
|
227
|
+
if (fromEnv !== void 0 && fromEnv.trim() !== "") return fromEnv;
|
|
228
|
+
try {
|
|
229
|
+
const fromFile = readDotenvValue(readFileSync(join(context.cwd, ".env"), "utf8"), KEY_VAR);
|
|
230
|
+
return fromFile === void 0 || fromFile.trim() === "" ? void 0 : fromFile;
|
|
231
|
+
} catch {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** Reads `--database <name>` or `--database=<name>`; a string when something is wrong. */
|
|
236
|
+
function readOptions(args) {
|
|
237
|
+
let database;
|
|
238
|
+
let help = false;
|
|
239
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
240
|
+
const arg = args[index] ?? "";
|
|
241
|
+
if (arg === "-h" || arg === "--help") help = true;
|
|
242
|
+
else if (arg === "--database") {
|
|
243
|
+
index += 1;
|
|
244
|
+
database = args[index];
|
|
245
|
+
if (database === void 0 || database.startsWith("-")) return "--database needs a name.";
|
|
246
|
+
} else if (arg.startsWith("--database=")) database = arg.slice(11);
|
|
247
|
+
else return `Unknown option ${arg}.`;
|
|
248
|
+
}
|
|
249
|
+
if (database === "") return "--database needs a name.";
|
|
250
|
+
return {
|
|
251
|
+
database,
|
|
252
|
+
help
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** Runs the `rilo-db` command and returns its exit code. It never prints the key. */
|
|
256
|
+
function runCli(argv, context) {
|
|
257
|
+
const [command, ...rest] = argv;
|
|
258
|
+
if (command === void 0 || command === "-h" || command === "--help" || command === "help") {
|
|
259
|
+
if (command === void 0) {
|
|
260
|
+
context.stderr(HELP);
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
context.stdout(HELP);
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
if (command !== "env") {
|
|
267
|
+
context.stderr(`rilo-db: Unknown command ${command}. Run rilo-db --help.\n`);
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
const options = readOptions(rest);
|
|
271
|
+
if (typeof options === "string") {
|
|
272
|
+
context.stderr(`rilo-db: ${options} Run rilo-db --help.\n`);
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
if (options.help) {
|
|
276
|
+
context.stdout(HELP);
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
const key = readKey(context);
|
|
280
|
+
if (key === void 0) {
|
|
281
|
+
context.stderr(`rilo-db: ${KEY_VAR} is not set. Put it in your environment or in a .env file in this folder.\n`);
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const { values } = resolveKey(key, {}, options.database);
|
|
286
|
+
context.stdout([
|
|
287
|
+
`RILODB_URI=${values.connectLink}`,
|
|
288
|
+
`${RILODB_LINK_ENV_VAR}=${values.rilodbLink}`,
|
|
289
|
+
`RILO_FILES_URL=${values.filesUrl}`,
|
|
290
|
+
`RILO_FILES_KEY=${values.filesKey}`,
|
|
291
|
+
""
|
|
292
|
+
].join("\n"));
|
|
293
|
+
return 0;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (!(error instanceof RiloKeyError)) throw error;
|
|
296
|
+
context.stderr(`rilo-db: ${error.message}\n`);
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// src/cli.ts
|
|
301
|
+
process.exitCode = runCli(process.argv.slice(2), {
|
|
302
|
+
env: process.env,
|
|
303
|
+
cwd: process.cwd(),
|
|
304
|
+
stdout: (text) => process.stdout.write(text),
|
|
305
|
+
stderr: (text) => process.stderr.write(text)
|
|
306
|
+
});
|
|
307
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The key or link is missing or isn't an app key or a `rilodb://` link. The message never repeats it. */
|
|
2
|
+
declare class RiloKeyError extends Error {
|
|
3
|
+
readonly name = "RiloKeyError";
|
|
4
|
+
}
|
|
5
|
+
/** What the Files API answers when a call fails: its `error` body, plus the HTTP status. */
|
|
6
|
+
declare class RiloFilesError extends Error {
|
|
7
|
+
readonly name = "RiloFilesError";
|
|
8
|
+
/** The HTTP status. */
|
|
9
|
+
readonly status: number;
|
|
10
|
+
/** A stable name for the error, like `read_only_key`. Decide what to do from this. */
|
|
11
|
+
readonly code: string;
|
|
12
|
+
/** Extra data for some codes, like `maxFileSize` on `file_too_large`. */
|
|
13
|
+
readonly details: Record<string, unknown> | null;
|
|
14
|
+
/** On `validation_failed`: each field that needs fixing, with what to fix. */
|
|
15
|
+
readonly fields: Record<string, string> | null;
|
|
16
|
+
constructor(init: {
|
|
17
|
+
status: number;
|
|
18
|
+
code: string;
|
|
19
|
+
message: string;
|
|
20
|
+
details?: Record<string, unknown> | null;
|
|
21
|
+
fields?: Record<string, string> | null;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export { RiloKeyError as n, RiloFilesError as t };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The key or link is missing or isn't an app key or a `rilodb://` link. The message never repeats it. */
|
|
2
|
+
declare class RiloKeyError extends Error {
|
|
3
|
+
readonly name = "RiloKeyError";
|
|
4
|
+
}
|
|
5
|
+
/** What the Files API answers when a call fails: its `error` body, plus the HTTP status. */
|
|
6
|
+
declare class RiloFilesError extends Error {
|
|
7
|
+
readonly name = "RiloFilesError";
|
|
8
|
+
/** The HTTP status. */
|
|
9
|
+
readonly status: number;
|
|
10
|
+
/** A stable name for the error, like `read_only_key`. Decide what to do from this. */
|
|
11
|
+
readonly code: string;
|
|
12
|
+
/** Extra data for some codes, like `maxFileSize` on `file_too_large`. */
|
|
13
|
+
readonly details: Record<string, unknown> | null;
|
|
14
|
+
/** On `validation_failed`: each field that needs fixing, with what to fix. */
|
|
15
|
+
readonly fields: Record<string, string> | null;
|
|
16
|
+
constructor(init: {
|
|
17
|
+
status: number;
|
|
18
|
+
code: string;
|
|
19
|
+
message: string;
|
|
20
|
+
details?: Record<string, unknown> | null;
|
|
21
|
+
fields?: Record<string, string> | null;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export { RiloKeyError as n, RiloFilesError as t };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_key = require("./key-DPXUxbGX.cjs");
|
|
3
|
+
let mongodb = require("mongodb");
|
|
4
|
+
// src/files.ts
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
/** One part of a path through `encodeURIComponent`, plus the `!'()*` it leaves as they are. */
|
|
9
|
+
function encodeSegment(segment) {
|
|
10
|
+
return encodeURIComponent(segment).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
11
|
+
}
|
|
12
|
+
/** A file path as it goes in a URL: each part encoded, the slashes between them kept. */
|
|
13
|
+
function encodePath(path) {
|
|
14
|
+
return path.split("/").map(encodeSegment).join("/");
|
|
15
|
+
}
|
|
16
|
+
/** The Files API's `error` body, as a `RiloFilesError`. Anything that isn't one still gets a status. */
|
|
17
|
+
async function toError(response) {
|
|
18
|
+
let error = {};
|
|
19
|
+
try {
|
|
20
|
+
const body = await response.json();
|
|
21
|
+
if (isRecord(body) && isRecord(body.error)) error = body.error;
|
|
22
|
+
} catch {}
|
|
23
|
+
const { code, message, details, fields } = error;
|
|
24
|
+
return new require_key.RiloFilesError({
|
|
25
|
+
status: response.status,
|
|
26
|
+
code: typeof code === "string" ? code : `http_${response.status}`,
|
|
27
|
+
message: typeof message === "string" ? message : `The Files API answered with status ${response.status}.`,
|
|
28
|
+
details: isRecord(details) ? details : null,
|
|
29
|
+
fields: isRecord(fields) ? fields : null
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The Files API client. `filesKey` is the `rdbf_…` part of the app key: it is the only secret that
|
|
34
|
+
* is ever sent, and only in the `Authorization` header.
|
|
35
|
+
*/
|
|
36
|
+
function createFiles(baseUrl, filesKey) {
|
|
37
|
+
async function call(method, path, body) {
|
|
38
|
+
const headers = {
|
|
39
|
+
authorization: `Bearer ${filesKey}`,
|
|
40
|
+
accept: "application/json"
|
|
41
|
+
};
|
|
42
|
+
if (body !== void 0) headers["content-type"] = "application/json";
|
|
43
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
44
|
+
method,
|
|
45
|
+
headers,
|
|
46
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) throw await toError(response);
|
|
49
|
+
if (response.status === 204) return void 0;
|
|
50
|
+
return response.json();
|
|
51
|
+
}
|
|
52
|
+
function bucket(name) {
|
|
53
|
+
const root = `/v1/buckets/${encodeURIComponent(name)}`;
|
|
54
|
+
async function makeLinks(target, options) {
|
|
55
|
+
const body = options?.expiresInSeconds === void 0 ? target : {
|
|
56
|
+
...target,
|
|
57
|
+
expiresInSeconds: options.expiresInSeconds
|
|
58
|
+
};
|
|
59
|
+
return (await call("POST", `${root}/links`, body)).links;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
createUpload: async (body) => await call("POST", `${root}/uploads`, body),
|
|
63
|
+
cancelUpload: async (uploadId) => {
|
|
64
|
+
await call("DELETE", `/v1/uploads/${encodeURIComponent(uploadId)}`);
|
|
65
|
+
},
|
|
66
|
+
link: async (path, options) => {
|
|
67
|
+
const [link] = await makeLinks({ path }, options);
|
|
68
|
+
if (link === void 0) throw new Error("The Files API answered without a link.");
|
|
69
|
+
return link;
|
|
70
|
+
},
|
|
71
|
+
links: (paths, options) => makeLinks({ paths }, options),
|
|
72
|
+
list: async (query = {}) => {
|
|
73
|
+
const params = new URLSearchParams();
|
|
74
|
+
if (query.prefix !== void 0) params.set("prefix", query.prefix);
|
|
75
|
+
if (query.delimiter !== void 0) params.set("delimiter", query.delimiter);
|
|
76
|
+
if (query.cursor !== void 0) params.set("cursor", query.cursor);
|
|
77
|
+
if (query.limit !== void 0) params.set("limit", String(query.limit));
|
|
78
|
+
const search = params.size === 0 ? "" : `?${params.toString()}`;
|
|
79
|
+
return await call("GET", `${root}/files${search}`);
|
|
80
|
+
},
|
|
81
|
+
get: async (path) => await call("GET", `${root}/files/${encodePath(path)}`),
|
|
82
|
+
delete: async (path) => {
|
|
83
|
+
await call("DELETE", `${root}/files/${encodePath(path)}`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
buckets: async () => await call("GET", "/v1/buckets"),
|
|
89
|
+
bucket
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// src/index.ts
|
|
93
|
+
const FILES_NEED_KEY = "Files needs your app key (RILO_KEY). A rilodb:// link holds only the database part.";
|
|
94
|
+
/**
|
|
95
|
+
* Turns an app key, or a `rilodb://` link, into everything an app needs. The app key (`rilo_…`)
|
|
96
|
+
* works for databases and Files. A `rilodb://` link works for databases only, and brings its own
|
|
97
|
+
* address and default database. Throws a `RiloKeyError` when the input is missing or is neither;
|
|
98
|
+
* the message says what was given and never repeats it. Nothing connects until `db()` is first used.
|
|
99
|
+
*/
|
|
100
|
+
function createClient(keyOrLink, options = {}) {
|
|
101
|
+
const target = require_key.resolveTarget(keyOrLink, options);
|
|
102
|
+
const files = target.files === null ? null : createFiles(target.files.url, target.files.key);
|
|
103
|
+
let mongo;
|
|
104
|
+
return {
|
|
105
|
+
db(name) {
|
|
106
|
+
const database = name ?? options.database ?? target.database ?? void 0;
|
|
107
|
+
if (database === void 0 || database === "") throw new TypeError("Say which database to open: db(\"name\"), or give createClient a `database` option.");
|
|
108
|
+
mongo ??= new mongodb.MongoClient(target.connectLink, options.appName === void 0 ? void 0 : { appName: options.appName });
|
|
109
|
+
return mongo.db(`${database}_${target.spaceId}`);
|
|
110
|
+
},
|
|
111
|
+
get files() {
|
|
112
|
+
if (files === null) throw new require_key.RiloKeyError(FILES_NEED_KEY);
|
|
113
|
+
return files;
|
|
114
|
+
},
|
|
115
|
+
async close() {
|
|
116
|
+
const open = mongo;
|
|
117
|
+
mongo = void 0;
|
|
118
|
+
await open?.close();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
exports.RiloFilesError = require_key.RiloFilesError;
|
|
123
|
+
exports.RiloKeyError = require_key.RiloKeyError;
|
|
124
|
+
exports.createClient = createClient;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { a as FileList, c as LinkOptions, d as UploadTicket, i as FileLink, l as ListFilesQuery, n as FileEntry, o as FilesBucketView, r as FileInfo, s as FilesBuckets, t as AddressOptions, u as UploadBody } from "./types-CD9NBWdA.cjs";
|
|
2
|
+
import { n as RiloKeyError, t as RiloFilesError } from "./errors-DwVg5fNj.cjs";
|
|
3
|
+
import { Db } from "mongodb";
|
|
4
|
+
/** What an app does with one bucket. */
|
|
5
|
+
interface RiloBucket {
|
|
6
|
+
/**
|
|
7
|
+
* Asks for a signed upload for one file. Send it to the browser, which posts the file straight
|
|
8
|
+
* to storage with `sendUpload` from `@riloservices/base/browser`. Needs a Read and write key.
|
|
9
|
+
*/
|
|
10
|
+
createUpload(body: UploadBody): Promise<UploadTicket>;
|
|
11
|
+
/** Releases an upload's hold on storage at once. Stop the browser's request first. */
|
|
12
|
+
cancelUpload(uploadId: string): Promise<void>;
|
|
13
|
+
/** A link to one file. A private bucket's link expires, one hour later unless you say. */
|
|
14
|
+
link(path: string, options?: LinkOptions): Promise<FileLink>;
|
|
15
|
+
/** Links to up to 100 files, in the order asked. */
|
|
16
|
+
links(paths: string[], options?: LinkOptions): Promise<FileLink[]>;
|
|
17
|
+
list(query?: ListFilesQuery): Promise<FileList>;
|
|
18
|
+
get(path: string): Promise<FileInfo>;
|
|
19
|
+
/** Needs a Read and write key. */
|
|
20
|
+
delete(path: string): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/** The Files API, called with the files part of the app key. */
|
|
23
|
+
interface RiloFiles {
|
|
24
|
+
buckets(): Promise<FilesBuckets>;
|
|
25
|
+
bucket(name: string): RiloBucket;
|
|
26
|
+
}
|
|
27
|
+
export interface ClientOptions extends AddressOptions {
|
|
28
|
+
/**
|
|
29
|
+
* The database `db()` opens when you give it no name. With a `rilodb://` link, the link's own
|
|
30
|
+
* database is the default when this isn't given.
|
|
31
|
+
*/
|
|
32
|
+
database?: string;
|
|
33
|
+
/** Shown in the Gateway's connection details, to tell apps apart. */
|
|
34
|
+
appName?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface RiloClient {
|
|
37
|
+
/**
|
|
38
|
+
* The MongoDB driver's own `Db`. Use the name you gave the database, like `salon-prod`: the
|
|
39
|
+
* client adds the Space's id. With no name, it opens `options.database`.
|
|
40
|
+
*/
|
|
41
|
+
db(name?: string): Db;
|
|
42
|
+
/**
|
|
43
|
+
* The Files API, with the files part of your app key. A `rilodb://` link has no files part, so
|
|
44
|
+
* with one, using `files` throws a `RiloKeyError`.
|
|
45
|
+
*/
|
|
46
|
+
readonly files: RiloFiles;
|
|
47
|
+
/** Closes the database connections. Call it when your app shuts down. */
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Turns an app key, or a `rilodb://` link, into everything an app needs. The app key (`rilo_…`)
|
|
52
|
+
* works for databases and Files. A `rilodb://` link works for databases only, and brings its own
|
|
53
|
+
* address and default database. Throws a `RiloKeyError` when the input is missing or is neither;
|
|
54
|
+
* the message says what was given and never repeats it. Nothing connects until `db()` is first used.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createClient(keyOrLink: string | null | undefined, options?: ClientOptions): RiloClient;
|
|
57
|
+
export { type AddressOptions, type FileEntry, type FileInfo, type FileLink, type FileList, type FilesBucketView, type FilesBuckets, type LinkOptions, type ListFilesQuery, type RiloBucket, type RiloFiles, RiloFilesError, RiloKeyError, type UploadBody, type UploadTicket };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { a as FileList, c as LinkOptions, d as UploadTicket, i as FileLink, l as ListFilesQuery, n as FileEntry, o as FilesBucketView, r as FileInfo, s as FilesBuckets, t as AddressOptions, u as UploadBody } from "./types-CD9NBWdA.mjs";
|
|
2
|
+
import { n as RiloKeyError, t as RiloFilesError } from "./errors-DwVg5fNj.mjs";
|
|
3
|
+
import { Db } from "mongodb";
|
|
4
|
+
/** What an app does with one bucket. */
|
|
5
|
+
interface RiloBucket {
|
|
6
|
+
/**
|
|
7
|
+
* Asks for a signed upload for one file. Send it to the browser, which posts the file straight
|
|
8
|
+
* to storage with `sendUpload` from `@riloservices/base/browser`. Needs a Read and write key.
|
|
9
|
+
*/
|
|
10
|
+
createUpload(body: UploadBody): Promise<UploadTicket>;
|
|
11
|
+
/** Releases an upload's hold on storage at once. Stop the browser's request first. */
|
|
12
|
+
cancelUpload(uploadId: string): Promise<void>;
|
|
13
|
+
/** A link to one file. A private bucket's link expires, one hour later unless you say. */
|
|
14
|
+
link(path: string, options?: LinkOptions): Promise<FileLink>;
|
|
15
|
+
/** Links to up to 100 files, in the order asked. */
|
|
16
|
+
links(paths: string[], options?: LinkOptions): Promise<FileLink[]>;
|
|
17
|
+
list(query?: ListFilesQuery): Promise<FileList>;
|
|
18
|
+
get(path: string): Promise<FileInfo>;
|
|
19
|
+
/** Needs a Read and write key. */
|
|
20
|
+
delete(path: string): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/** The Files API, called with the files part of the app key. */
|
|
23
|
+
interface RiloFiles {
|
|
24
|
+
buckets(): Promise<FilesBuckets>;
|
|
25
|
+
bucket(name: string): RiloBucket;
|
|
26
|
+
}
|
|
27
|
+
export interface ClientOptions extends AddressOptions {
|
|
28
|
+
/**
|
|
29
|
+
* The database `db()` opens when you give it no name. With a `rilodb://` link, the link's own
|
|
30
|
+
* database is the default when this isn't given.
|
|
31
|
+
*/
|
|
32
|
+
database?: string;
|
|
33
|
+
/** Shown in the Gateway's connection details, to tell apps apart. */
|
|
34
|
+
appName?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface RiloClient {
|
|
37
|
+
/**
|
|
38
|
+
* The MongoDB driver's own `Db`. Use the name you gave the database, like `salon-prod`: the
|
|
39
|
+
* client adds the Space's id. With no name, it opens `options.database`.
|
|
40
|
+
*/
|
|
41
|
+
db(name?: string): Db;
|
|
42
|
+
/**
|
|
43
|
+
* The Files API, with the files part of your app key. A `rilodb://` link has no files part, so
|
|
44
|
+
* with one, using `files` throws a `RiloKeyError`.
|
|
45
|
+
*/
|
|
46
|
+
readonly files: RiloFiles;
|
|
47
|
+
/** Closes the database connections. Call it when your app shuts down. */
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Turns an app key, or a `rilodb://` link, into everything an app needs. The app key (`rilo_…`)
|
|
52
|
+
* works for databases and Files. A `rilodb://` link works for databases only, and brings its own
|
|
53
|
+
* address and default database. Throws a `RiloKeyError` when the input is missing or is neither;
|
|
54
|
+
* the message says what was given and never repeats it. Nothing connects until `db()` is first used.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createClient(keyOrLink: string | null | undefined, options?: ClientOptions): RiloClient;
|
|
57
|
+
export { type AddressOptions, type FileEntry, type FileInfo, type FileLink, type FileList, type FilesBucketView, type FilesBuckets, type LinkOptions, type ListFilesQuery, type RiloBucket, type RiloFiles, RiloFilesError, RiloKeyError, type UploadBody, type UploadTicket };
|