@rebasepro/types 0.22.0 → 0.24.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/dist/controllers/client.d.ts +61 -17
- package/dist/controllers/collection_registry.d.ts +17 -0
- package/dist/controllers/data.d.ts +161 -49
- package/dist/controllers/data_driver.d.ts +10 -0
- package/dist/controllers/storage.d.ts +132 -13
- package/dist/errors.d.ts +15 -1
- package/dist/index.es.js +1094 -37
- package/dist/index.es.js.map +1 -1
- package/dist/types/admin_block.d.ts +1 -1
- package/dist/types/api_keys.d.ts +68 -36
- package/dist/types/auth_adapter.d.ts +99 -1
- package/dist/types/backend.d.ts +222 -3
- package/dist/types/backup.d.ts +83 -0
- package/dist/types/collection_patch.d.ts +74 -0
- package/dist/types/collections.d.ts +23 -0
- package/dist/types/cron.d.ts +44 -5
- package/dist/types/database_adapter.d.ts +6 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/postgres_introspection.d.ts +10 -0
- package/dist/types/project_manifest.d.ts +65 -1
- package/dist/types/properties.d.ts +11 -0
- package/dist/types/schema_editing.d.ts +40 -14
- package/dist/types/scopes.d.ts +272 -0
- package/dist/types/security_rules.d.ts +28 -1
- package/dist/types/storage_authorize.d.ts +5 -1
- package/dist/users/user.d.ts +7 -0
- package/package.json +2 -2
package/dist/index.es.js
CHANGED
|
@@ -499,6 +499,7 @@ var ADMIN_PROPERTY_KEYS = [
|
|
|
499
499
|
"disabled",
|
|
500
500
|
"expanded",
|
|
501
501
|
"Field",
|
|
502
|
+
"filledByServer",
|
|
502
503
|
"Filter",
|
|
503
504
|
"filterOperators",
|
|
504
505
|
"fixedFilter",
|
|
@@ -700,12 +701,29 @@ var DEFAULT_CAPABILITIES = {
|
|
|
700
701
|
supportsDocumentAdmin: true,
|
|
701
702
|
supportsSchemaAdmin: true
|
|
702
703
|
};
|
|
703
|
-
var
|
|
704
|
-
postgres
|
|
705
|
-
firestore
|
|
706
|
-
mongodb
|
|
707
|
-
"(default)"
|
|
708
|
-
|
|
704
|
+
var BUILT_IN_CAPABILITIES = /* @__PURE__ */ new Map([
|
|
705
|
+
["postgres", POSTGRES_CAPABILITIES],
|
|
706
|
+
["firestore", FIREBASE_CAPABILITIES],
|
|
707
|
+
["mongodb", MONGODB_CAPABILITIES],
|
|
708
|
+
["(default)", DEFAULT_CAPABILITIES]
|
|
709
|
+
]);
|
|
710
|
+
/**
|
|
711
|
+
* Capabilities registered at runtime, on `globalThis` rather than in a module
|
|
712
|
+
* local: a third-party driver inlines its own copy of this package and
|
|
713
|
+
* registers through it, while the app and the server read through theirs. A
|
|
714
|
+
* module local made the registration land in a copy nobody else reads.
|
|
715
|
+
* See `module-state-across-copies.test.ts`.
|
|
716
|
+
*/
|
|
717
|
+
var REGISTERED_CAPABILITIES_KEY = Symbol.for("@rebasepro/types.dataSourceCapabilities.v1");
|
|
718
|
+
function registeredCapabilities() {
|
|
719
|
+
const g = globalThis;
|
|
720
|
+
let registered = g[REGISTERED_CAPABILITIES_KEY];
|
|
721
|
+
if (!registered) {
|
|
722
|
+
registered = /* @__PURE__ */ new Map();
|
|
723
|
+
g[REGISTERED_CAPABILITIES_KEY] = registered;
|
|
724
|
+
}
|
|
725
|
+
return registered;
|
|
726
|
+
}
|
|
709
727
|
/**
|
|
710
728
|
* Look up capabilities for a given engine key.
|
|
711
729
|
* If `engine` is undefined or not found, returns `DEFAULT_CAPABILITIES`.
|
|
@@ -713,14 +731,14 @@ var CAPABILITIES_REGISTRY = {
|
|
|
713
731
|
*/
|
|
714
732
|
function getDataSourceCapabilities(engine) {
|
|
715
733
|
if (!engine) return POSTGRES_CAPABILITIES;
|
|
716
|
-
return
|
|
734
|
+
return registeredCapabilities().get(engine) ?? BUILT_IN_CAPABILITIES.get(engine) ?? DEFAULT_CAPABILITIES;
|
|
717
735
|
}
|
|
718
736
|
/**
|
|
719
737
|
* Register custom capabilities for a third-party driver.
|
|
720
738
|
* @group Models
|
|
721
739
|
*/
|
|
722
740
|
function registerDataSourceCapabilities(capabilities) {
|
|
723
|
-
|
|
741
|
+
registeredCapabilities().set(capabilities.key, capabilities);
|
|
724
742
|
}
|
|
725
743
|
//#endregion
|
|
726
744
|
//#region src/types/collections.ts
|
|
@@ -1131,14 +1149,281 @@ function isBranchAdmin(admin) {
|
|
|
1131
1149
|
}
|
|
1132
1150
|
//#endregion
|
|
1133
1151
|
//#region src/types/schema_editing.ts
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1152
|
+
/**
|
|
1153
|
+
* The SQL `db push` and `db generate` render — `schema.sql`, `policies.sql` and
|
|
1154
|
+
* the rest — is not here, and must not come back. Both commands regenerate it
|
|
1155
|
+
* from the collections before reading it, so nothing ever read a committed
|
|
1156
|
+
* copy; it lives in the gitignored `.rebase/sql/`. When the commit carried it,
|
|
1157
|
+
* it carried a second copy besides: these paths are from the project root,
|
|
1158
|
+
* while the CLI writes beside the backend, so a live edit left `drizzle/` and
|
|
1159
|
+
* `backend/drizzle/` disagreeing about the schema.
|
|
1160
|
+
*/
|
|
1161
|
+
var DEFAULT_COMMIT_PATHS = { schemaFile: "backend/src/schema.generated.ts" };
|
|
1162
|
+
//#endregion
|
|
1163
|
+
//#region src/types/collection_patch.ts
|
|
1164
|
+
/**
|
|
1165
|
+
* What changed about a collection, as opposed to what the collection is.
|
|
1166
|
+
*
|
|
1167
|
+
* ## Why a save is a patch
|
|
1168
|
+
*
|
|
1169
|
+
* The admin panel edits a collection as JSON — the only form a browser and a
|
|
1170
|
+
* server can exchange it in — and JSON cannot carry most of what a collection
|
|
1171
|
+
* file holds: an entity action's `onClick`, a computed field's `value`, a
|
|
1172
|
+
* property shared from another module (`status: statusProperty`), an enum
|
|
1173
|
+
* imported from elsewhere (`enum: LOCALE_ENUM`), the comments around all of
|
|
1174
|
+
* them. A save that posted the whole collection and had it written back
|
|
1175
|
+
* rewrote every key it carried and deleted everything JSON had dropped, so
|
|
1176
|
+
* renaming a collection removed its actions' handlers, inlined its shared
|
|
1177
|
+
* property without the custom `Field`, and froze a copy of the enum.
|
|
1178
|
+
*
|
|
1179
|
+
* So a save sends what the person changed: the difference between the JSON
|
|
1180
|
+
* the editor loaded and the JSON it is saving. Anything JSON could not carry
|
|
1181
|
+
* is identical on both sides of that difference — dropped from both — and
|
|
1182
|
+
* cannot appear in it. The server applies the patch to the collection as it
|
|
1183
|
+
* is (for the plan) and writes only the patched keys into the file.
|
|
1184
|
+
*
|
|
1185
|
+
* ## The shape
|
|
1186
|
+
*
|
|
1187
|
+
* Two operations on paths of object keys. Arrays are values: an array that
|
|
1188
|
+
* changed is set whole. Positions in an array are not stable identities —
|
|
1189
|
+
* element 2 of an enum is not "the third value" once a value is inserted —
|
|
1190
|
+
* and the writer is the one place that can match an old element to its new
|
|
1191
|
+
* self (by `key`/`id`/`name`) and keep what JSON dropped from it.
|
|
1192
|
+
*
|
|
1193
|
+
* Paths are in the AUTHORING shape — presentation keys under `admin`, the
|
|
1194
|
+
* shape the file has — see {@link nestCollectionPatchPaths}.
|
|
1195
|
+
*/
|
|
1196
|
+
/**
|
|
1197
|
+
* Keys the runtime adds to a collection that must never be written into its
|
|
1198
|
+
* source. `resolvedRelation` is the registry's resolved form of a relation; a
|
|
1199
|
+
* file holding one does not type-check (`target` is missing from it).
|
|
1200
|
+
*/
|
|
1201
|
+
var RUNTIME_ONLY_COLLECTION_KEYS = ["resolvedRelation"];
|
|
1202
|
+
/** Path segments that reach an object's prototype rather than a key of it. */
|
|
1203
|
+
var UNSAFE_SEGMENTS = /* @__PURE__ */ new Set([
|
|
1204
|
+
"__proto__",
|
|
1205
|
+
"constructor",
|
|
1206
|
+
"prototype"
|
|
1207
|
+
]);
|
|
1208
|
+
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1209
|
+
/** A JSON value as JSON would carry it: functions, `undefined` and symbols dropped. */
|
|
1210
|
+
function toCollectionJson(value) {
|
|
1211
|
+
if (value === void 0) return void 0;
|
|
1212
|
+
const text = JSON.stringify(value);
|
|
1213
|
+
return text === void 0 ? void 0 : JSON.parse(text);
|
|
1214
|
+
}
|
|
1215
|
+
function jsonEqual(a, b) {
|
|
1216
|
+
if (a === b) return true;
|
|
1217
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => jsonEqual(item, b[index]));
|
|
1218
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
1219
|
+
const keys = Object.keys(a);
|
|
1220
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
1221
|
+
return keys.every((key) => Object.prototype.hasOwnProperty.call(b, key) && jsonEqual(a[key], b[key]));
|
|
1222
|
+
}
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* The operations that take `before` to `after`, both JSON values.
|
|
1227
|
+
*
|
|
1228
|
+
* Objects are compared key by key; anything else that differs — a primitive,
|
|
1229
|
+
* an array, a change of kind — is set whole.
|
|
1230
|
+
*/
|
|
1231
|
+
function diffJsonValues(before, after, path = []) {
|
|
1232
|
+
if (jsonEqual(before, after)) return [];
|
|
1233
|
+
if (isPlainObject(before) && isPlainObject(after)) {
|
|
1234
|
+
const ops = [];
|
|
1235
|
+
for (const key of Object.keys(after)) if (!Object.prototype.hasOwnProperty.call(before, key)) ops.push({
|
|
1236
|
+
op: "set",
|
|
1237
|
+
path: [...path, key],
|
|
1238
|
+
value: after[key]
|
|
1239
|
+
});
|
|
1240
|
+
else ops.push(...diffJsonValues(before[key], after[key], [...path, key]));
|
|
1241
|
+
for (const key of Object.keys(before)) if (!Object.prototype.hasOwnProperty.call(after, key)) ops.push({
|
|
1242
|
+
op: "remove",
|
|
1243
|
+
path: [...path, key]
|
|
1244
|
+
});
|
|
1245
|
+
return ops;
|
|
1246
|
+
}
|
|
1247
|
+
return [{
|
|
1248
|
+
op: "set",
|
|
1249
|
+
path,
|
|
1250
|
+
value: after
|
|
1251
|
+
}];
|
|
1252
|
+
}
|
|
1253
|
+
function step(position, segment) {
|
|
1254
|
+
switch (position) {
|
|
1255
|
+
case "collection": return segment === "properties" ? "properties" : "other";
|
|
1256
|
+
case "properties": return "property";
|
|
1257
|
+
case "property":
|
|
1258
|
+
if (segment === "properties") return "properties";
|
|
1259
|
+
if (segment === "of") return "property";
|
|
1260
|
+
if (segment === "oneOf") return "oneOf";
|
|
1261
|
+
return "other";
|
|
1262
|
+
case "oneOf": return segment === "properties" ? "properties" : "other";
|
|
1263
|
+
default: return "other";
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function nestPath(path) {
|
|
1267
|
+
const nested = [];
|
|
1268
|
+
let position = "collection";
|
|
1269
|
+
for (const segment of path) {
|
|
1270
|
+
const moved = position === "collection" && ADMIN_COLLECTION_KEYS.includes(segment) || position === "property" && ADMIN_PROPERTY_KEYS.includes(segment);
|
|
1271
|
+
if (moved) nested.push("admin");
|
|
1272
|
+
nested.push(segment);
|
|
1273
|
+
position = moved ? "other" : step(position, segment);
|
|
1274
|
+
}
|
|
1275
|
+
return {
|
|
1276
|
+
path: nested,
|
|
1277
|
+
position
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
function nestValue(position, value) {
|
|
1281
|
+
if (position === "property") {
|
|
1282
|
+
if (Array.isArray(value)) return value.map((entry) => isPlainObject(entry) ? nestAdminPropertyKeys(entry) : entry);
|
|
1283
|
+
return isPlainObject(value) ? nestAdminPropertyKeys(value) : value;
|
|
1284
|
+
}
|
|
1285
|
+
if (position === "properties" && isPlainObject(value)) return Object.fromEntries(Object.entries(value).map(([key, property]) => [key, isPlainObject(property) ? nestAdminPropertyKeys(property) : property]));
|
|
1286
|
+
if (position === "oneOf" && isPlainObject(value)) return isPlainObject(value.properties) ? {
|
|
1287
|
+
...value,
|
|
1288
|
+
properties: nestValue("properties", value.properties)
|
|
1289
|
+
} : value;
|
|
1290
|
+
if (position === "collection" && isPlainObject(value)) {
|
|
1291
|
+
const nested = nestAdminCollectionKeys(value);
|
|
1292
|
+
return isPlainObject(nested.properties) ? {
|
|
1293
|
+
...nested,
|
|
1294
|
+
properties: nestValue("properties", nested.properties)
|
|
1295
|
+
} : nested;
|
|
1296
|
+
}
|
|
1297
|
+
return value;
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* The patch, with every flat presentation key moved into its `admin` block.
|
|
1301
|
+
*
|
|
1302
|
+
* The panel edits a flat view model — `icon` beside `name`, a property's
|
|
1303
|
+
* `readOnly` beside its `type` — while the file keeps them under `admin`. The
|
|
1304
|
+
* difference is taken on the flat model, because that is where a cleared field
|
|
1305
|
+
* shows up as a removed key (the `admin` copy the model was loaded with still
|
|
1306
|
+
* holds the old value), and moved here.
|
|
1307
|
+
*
|
|
1308
|
+
* The flat value wins over the `admin` copy, as in {@link nestAdminKeysOf}:
|
|
1309
|
+
* operations that were already under `admin` are ordered first, so the flat
|
|
1310
|
+
* one — what the form just wrote — is applied last.
|
|
1311
|
+
*/
|
|
1312
|
+
function nestCollectionPatchPaths(patch) {
|
|
1313
|
+
return patch.map((op, index) => {
|
|
1314
|
+
const { path, position } = nestPath(op.path);
|
|
1315
|
+
const wasNested = op.path.includes("admin");
|
|
1316
|
+
return {
|
|
1317
|
+
moved: op.op === "remove" ? {
|
|
1318
|
+
op: "remove",
|
|
1319
|
+
path
|
|
1320
|
+
} : {
|
|
1321
|
+
op: "set",
|
|
1322
|
+
path,
|
|
1323
|
+
value: nestValue(position, op.value)
|
|
1324
|
+
},
|
|
1325
|
+
wasNested,
|
|
1326
|
+
index
|
|
1327
|
+
};
|
|
1328
|
+
}).sort((a, b) => a.wasNested === b.wasNested ? a.index - b.index : a.wasNested ? -1 : 1).map((entry) => entry.moved);
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* The patch that takes the collection the editor loaded to the one it is
|
|
1332
|
+
* saving, in the authoring shape. Empty when nothing changed.
|
|
1333
|
+
*
|
|
1334
|
+
* Both sides go through JSON first, so whatever JSON cannot carry — a
|
|
1335
|
+
* function, a value it drops — is equally absent from both and never appears
|
|
1336
|
+
* as a change.
|
|
1337
|
+
*/
|
|
1338
|
+
function diffCollections(loaded, saving) {
|
|
1339
|
+
return nestCollectionPatchPaths(diffJsonValues(toCollectionJson(loaded), toCollectionJson(saving)));
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Why a patch may not be applied, or an empty list.
|
|
1343
|
+
*
|
|
1344
|
+
* Checked by every door that accepts one: a path is a list of non-empty object
|
|
1345
|
+
* keys that cannot reach a prototype, and no runtime-only key may be written.
|
|
1346
|
+
*/
|
|
1347
|
+
function collectionPatchProblems(patch) {
|
|
1348
|
+
if (!Array.isArray(patch)) return ["`patch` must be a list of operations."];
|
|
1349
|
+
const problems = [];
|
|
1350
|
+
patch.forEach((op, index) => {
|
|
1351
|
+
const where = `patch[${index}]`;
|
|
1352
|
+
if (!isPlainObject(op) || op.op !== "set" && op.op !== "remove") {
|
|
1353
|
+
problems.push(`${where} must be { op: "set", path, value } or { op: "remove", path }.`);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
const path = op.path;
|
|
1357
|
+
if (!Array.isArray(path) || path.length === 0 || !path.every((segment) => typeof segment === "string" && segment.length > 0)) {
|
|
1358
|
+
problems.push(`${where}.path must be a non-empty list of object keys.`);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const unsafe = path.find((segment) => UNSAFE_SEGMENTS.has(segment));
|
|
1362
|
+
if (unsafe) problems.push(`${where}.path may not contain "${unsafe}".`);
|
|
1363
|
+
const runtime = path.find((segment) => RUNTIME_ONLY_COLLECTION_KEYS.includes(segment));
|
|
1364
|
+
if (runtime) problems.push(`${where} writes "${runtime}", which the runtime adds and a collection file must not hold.`);
|
|
1365
|
+
if (op.op === "set") {
|
|
1366
|
+
const key = keyIn(op.value, (key) => UNSAFE_SEGMENTS.has(key) || RUNTIME_ONLY_COLLECTION_KEYS.includes(key));
|
|
1367
|
+
if (key) problems.push(`${where}.value holds "${key}", which a collection file must not hold.`);
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
return problems;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* The first runtime-only key (see {@link RUNTIME_ONLY_COLLECTION_KEYS}) found
|
|
1374
|
+
* anywhere in a value, or `undefined`. For the doors that take a whole
|
|
1375
|
+
* collection rather than a patch.
|
|
1376
|
+
*/
|
|
1377
|
+
function runtimeOnlyKeyIn(value) {
|
|
1378
|
+
return keyIn(value, (key) => RUNTIME_ONLY_COLLECTION_KEYS.includes(key));
|
|
1379
|
+
}
|
|
1380
|
+
/** A patch every door may apply: {@link collectionPatchProblems} found nothing. */
|
|
1381
|
+
function isCollectionPatch(patch) {
|
|
1382
|
+
return collectionPatchProblems(patch).length === 0;
|
|
1383
|
+
}
|
|
1384
|
+
function keyIn(value, matches) {
|
|
1385
|
+
if (Array.isArray(value)) {
|
|
1386
|
+
for (const item of value) {
|
|
1387
|
+
const found = keyIn(item, matches);
|
|
1388
|
+
if (found) return found;
|
|
1389
|
+
}
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
if (!isPlainObject(value)) return void 0;
|
|
1393
|
+
for (const key of Object.keys(value)) {
|
|
1394
|
+
if (matches(key)) return key;
|
|
1395
|
+
const found = keyIn(value[key], matches);
|
|
1396
|
+
if (found) return found;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* `target` with the patch applied, copying only what it changes.
|
|
1401
|
+
*
|
|
1402
|
+
* Everything the patch does not reach is shared with `target` by reference —
|
|
1403
|
+
* functions, thunks and all — which is what lets a planner see a relation's
|
|
1404
|
+
* `target()` on the patched collection exactly as it was. Intermediate objects
|
|
1405
|
+
* a `set` needs are created; a `remove` of a key that is not there is nothing
|
|
1406
|
+
* to do.
|
|
1407
|
+
*/
|
|
1408
|
+
function applyCollectionPatch(target, patch) {
|
|
1409
|
+
let root = target;
|
|
1410
|
+
for (const op of patch) root = applyOp(root, op.path, op);
|
|
1411
|
+
return root;
|
|
1412
|
+
}
|
|
1413
|
+
function applyOp(node, path, op) {
|
|
1414
|
+
const [head, ...rest] = path;
|
|
1415
|
+
if (UNSAFE_SEGMENTS.has(head)) throw new Error(`A collection patch may not reach "${head}".`);
|
|
1416
|
+
const base = isPlainObject(node) ? { ...node } : {};
|
|
1417
|
+
if (rest.length === 0) {
|
|
1418
|
+
if (op.op === "remove") delete base[head];
|
|
1419
|
+
else base[head] = op.value;
|
|
1420
|
+
return base;
|
|
1421
|
+
}
|
|
1422
|
+
const child = Object.prototype.hasOwnProperty.call(base, head) ? base[head] : void 0;
|
|
1423
|
+
if (op.op === "remove" && !isPlainObject(child)) return node;
|
|
1424
|
+
base[head] = applyOp(child, rest, op);
|
|
1425
|
+
return base;
|
|
1426
|
+
}
|
|
1142
1427
|
//#endregion
|
|
1143
1428
|
//#region src/types/channel_bus.ts
|
|
1144
1429
|
/**
|
|
@@ -1663,10 +1948,15 @@ amendResourceKind("bucket", {
|
|
|
1663
1948
|
"S3_FORCE_PATH_STYLE",
|
|
1664
1949
|
"GCS_BUCKET",
|
|
1665
1950
|
"GCS_PROJECT_ID",
|
|
1666
|
-
"GCS_KEY_FILENAME"
|
|
1951
|
+
"GCS_KEY_FILENAME",
|
|
1952
|
+
"STORAGE_MAX_FILE_SIZE"
|
|
1667
1953
|
],
|
|
1668
1954
|
envBasesByEngine: {
|
|
1669
|
-
local: [
|
|
1955
|
+
local: [
|
|
1956
|
+
"STORAGE_TYPE",
|
|
1957
|
+
"STORAGE_PATH",
|
|
1958
|
+
"STORAGE_MAX_FILE_SIZE"
|
|
1959
|
+
],
|
|
1670
1960
|
s3: [
|
|
1671
1961
|
"STORAGE_TYPE",
|
|
1672
1962
|
"S3_BUCKET",
|
|
@@ -1674,13 +1964,15 @@ amendResourceKind("bucket", {
|
|
|
1674
1964
|
"S3_ACCESS_KEY_ID",
|
|
1675
1965
|
"S3_SECRET_ACCESS_KEY",
|
|
1676
1966
|
"S3_ENDPOINT",
|
|
1677
|
-
"S3_FORCE_PATH_STYLE"
|
|
1967
|
+
"S3_FORCE_PATH_STYLE",
|
|
1968
|
+
"STORAGE_MAX_FILE_SIZE"
|
|
1678
1969
|
],
|
|
1679
1970
|
gcs: [
|
|
1680
1971
|
"STORAGE_TYPE",
|
|
1681
1972
|
"GCS_BUCKET",
|
|
1682
1973
|
"GCS_PROJECT_ID",
|
|
1683
|
-
"GCS_KEY_FILENAME"
|
|
1974
|
+
"GCS_KEY_FILENAME",
|
|
1975
|
+
"STORAGE_MAX_FILE_SIZE"
|
|
1684
1976
|
],
|
|
1685
1977
|
azure: [],
|
|
1686
1978
|
firebase: []
|
|
@@ -1698,19 +1990,73 @@ registerResourceKind({
|
|
|
1698
1990
|
implicitDefault: false
|
|
1699
1991
|
});
|
|
1700
1992
|
amendResourceKind("topic", { envBases: [] });
|
|
1701
|
-
|
|
1993
|
+
/**
|
|
1994
|
+
* Declared handlers and installed runtimes, for topics and queues — on
|
|
1995
|
+
* `globalThis` under a shared symbol, never in module locals.
|
|
1996
|
+
*
|
|
1997
|
+
* A module local is one per COPY of this package, and two copies in one
|
|
1998
|
+
* process is the normal layout rather than an accident: `@rebasepro/server`
|
|
1999
|
+
* inlines this package into its own dist, while `config/resources.ts` imports
|
|
2000
|
+
* it from `node_modules`. With locals, a project's `.handler()` and
|
|
2001
|
+
* `.subscription()` went into the project's copy and the worker read the
|
|
2002
|
+
* server's empty one (`Job queue started {"tasks":0}`), and the runtime the
|
|
2003
|
+
* server installed was invisible to the project's `enqueue`/`publish` — so
|
|
2004
|
+
* every queue and topic threw "no runtime is installed" inside a running
|
|
2005
|
+
* backend. The CLI deriving the graph had the same split. Same mechanism and
|
|
2006
|
+
* reasoning as `registry()` in resources.ts; `module-state-across-copies.test.ts`
|
|
2007
|
+
* loads two copies to hold it, and scans for any other module-level state that
|
|
2008
|
+
* would split the same way.
|
|
2009
|
+
*
|
|
2010
|
+
* The version is in the symbol: a change to this object's SHAPE takes a new
|
|
2011
|
+
* suffix, so no copy ever reads a shape it does not know.
|
|
2012
|
+
*/
|
|
2013
|
+
var WORK_KEY = Symbol.for("@rebasepro/types.workRegistry.v1");
|
|
2014
|
+
function work() {
|
|
2015
|
+
const g = globalThis;
|
|
2016
|
+
let shared = g[WORK_KEY];
|
|
2017
|
+
if (!shared) {
|
|
2018
|
+
shared = {
|
|
2019
|
+
topicRuntime: null,
|
|
2020
|
+
subscriptions: [],
|
|
2021
|
+
queueRuntime: null,
|
|
2022
|
+
queueConsumers: /* @__PURE__ */ new Map()
|
|
2023
|
+
};
|
|
2024
|
+
g[WORK_KEY] = shared;
|
|
2025
|
+
}
|
|
2026
|
+
return shared;
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Where `@rebasepro/server` publishes its client once it has booted (its
|
|
2030
|
+
* `singleton.ts`). Read only to tell the two causes of a missing runtime apart.
|
|
2031
|
+
*/
|
|
2032
|
+
var SERVER_INSTANCE_SLOT = Symbol.for("@rebasepro/server:singleton-instance");
|
|
2033
|
+
/**
|
|
2034
|
+
* Why there is no runtime to send through, as far as this process can tell.
|
|
2035
|
+
*
|
|
2036
|
+
* Outside a backend — the CLI evaluating config, a script, a unit test — the
|
|
2037
|
+
* answer is that there is nothing to deliver to. Inside one it is not: the
|
|
2038
|
+
* backend installs a runtime at boot for what `config/resources.ts` declared,
|
|
2039
|
+
* so a missing one there means boot never saw the declaration, or the backend
|
|
2040
|
+
* carries a copy of this package too old to share the slot it installs into.
|
|
2041
|
+
* Saying "outside a backend" to somebody whose backend is running sends them
|
|
2042
|
+
* looking for the wrong thing.
|
|
2043
|
+
*/
|
|
2044
|
+
function missingRuntimeCause(kind) {
|
|
2045
|
+
if (!(globalThis[SERVER_INSTANCE_SLOT] != null)) return "This works inside a running Rebase backend; this looks like code running outside one (a build, a script, or a test without a harness).";
|
|
2046
|
+
return `A Rebase backend is running in this process but installed no ${kind} runtime. It installs one at boot when config/resources.ts declares a ${kind}, so either this ${kind} is declared somewhere boot does not read, or the backend's copy of @rebasepro/types predates the shared runtime slot this copy reads — keep @rebasepro/server and @rebasepro/types on the same version.`;
|
|
2047
|
+
}
|
|
1702
2048
|
/** Install the transport topics publish through. Called by the server at boot. */
|
|
1703
2049
|
function setTopicRuntime(runtime) {
|
|
1704
|
-
|
|
2050
|
+
work().topicRuntime = runtime;
|
|
1705
2051
|
}
|
|
1706
|
-
var subscriptions = [];
|
|
1707
2052
|
/** Every declared subscription, for the worker to wire and the graph to record. */
|
|
1708
2053
|
function declaredSubscriptions(topic) {
|
|
2054
|
+
const { subscriptions } = work();
|
|
1709
2055
|
return topic ? subscriptions.filter((s) => s.topic === topic) : subscriptions.slice();
|
|
1710
2056
|
}
|
|
1711
2057
|
/** Forget declared subscriptions. For tests, alongside `resetDeclaredResources`. */
|
|
1712
2058
|
function resetDeclaredSubscriptions() {
|
|
1713
|
-
subscriptions.length = 0;
|
|
2059
|
+
work().subscriptions.length = 0;
|
|
1714
2060
|
}
|
|
1715
2061
|
/**
|
|
1716
2062
|
* Declare a topic.
|
|
@@ -1729,12 +2075,13 @@ function topic(key, options = {}) {
|
|
|
1729
2075
|
return key;
|
|
1730
2076
|
},
|
|
1731
2077
|
async publish(event) {
|
|
1732
|
-
const runtime =
|
|
1733
|
-
if (!runtime) throw new Error(`Cannot publish to topic "${key}": no topic runtime is installed.
|
|
2078
|
+
const runtime = work().topicRuntime;
|
|
2079
|
+
if (!runtime) throw new Error(`Cannot publish to topic "${key}": no topic runtime is installed. ${missingRuntimeCause("topic")}`);
|
|
1734
2080
|
await runtime.publish(key, event);
|
|
1735
2081
|
},
|
|
1736
2082
|
subscription(name, handler, subOptions = {}) {
|
|
1737
2083
|
if (!name || name.trim() === "") throw new Error(`A subscription on topic "${key}" needs a non-empty name.`);
|
|
2084
|
+
const { subscriptions } = work();
|
|
1738
2085
|
if (subscriptions.some((s) => s.topic === key && s.name === name)) throw new Error(`Topic "${key}" already has a subscription named "${name}". The name is what a job row records and what a retry counts against, so two cannot share one.`);
|
|
1739
2086
|
subscriptions.push({
|
|
1740
2087
|
topic: key,
|
|
@@ -1797,19 +2144,17 @@ registerResourceKind({
|
|
|
1797
2144
|
optionKeys: ["maxAttempts"],
|
|
1798
2145
|
implicitDefault: false
|
|
1799
2146
|
});
|
|
1800
|
-
var queueRuntimeHolder = { current: null };
|
|
1801
2147
|
/** Install the transport queues enqueue through. Called by the server at boot. */
|
|
1802
2148
|
function setQueueRuntime(runtime) {
|
|
1803
|
-
|
|
2149
|
+
work().queueRuntime = runtime;
|
|
1804
2150
|
}
|
|
1805
|
-
var queueConsumers = /* @__PURE__ */ new Map();
|
|
1806
2151
|
/** Every declared queue handler, for the worker to wire. */
|
|
1807
2152
|
function declaredQueueConsumers() {
|
|
1808
|
-
return [...queueConsumers.values()];
|
|
2153
|
+
return [...work().queueConsumers.values()];
|
|
1809
2154
|
}
|
|
1810
2155
|
/** Forget declared queue handlers. For tests, alongside `resetDeclaredResources`. */
|
|
1811
2156
|
function resetDeclaredQueueConsumers() {
|
|
1812
|
-
queueConsumers.clear();
|
|
2157
|
+
work().queueConsumers.clear();
|
|
1813
2158
|
}
|
|
1814
2159
|
/**
|
|
1815
2160
|
* Declare a queue.
|
|
@@ -1831,11 +2176,12 @@ function queue(key, options = {}) {
|
|
|
1831
2176
|
return key;
|
|
1832
2177
|
},
|
|
1833
2178
|
async enqueue(payload, enqueueOptions) {
|
|
1834
|
-
const runtime =
|
|
1835
|
-
if (!runtime) throw new Error(`Cannot enqueue on queue "${key}": no queue runtime is installed.
|
|
2179
|
+
const runtime = work().queueRuntime;
|
|
2180
|
+
if (!runtime) throw new Error(`Cannot enqueue on queue "${key}": no queue runtime is installed. ${missingRuntimeCause("queue")}`);
|
|
1836
2181
|
return runtime.enqueue(key, payload, enqueueOptions);
|
|
1837
2182
|
},
|
|
1838
2183
|
handler(fn) {
|
|
2184
|
+
const { queueConsumers } = work();
|
|
1839
2185
|
if (queueConsumers.has(key)) throw new Error(`Queue "${key}" already has a handler. A queue has exactly one consumer; work that several things react to is a topic with several subscriptions.`);
|
|
1840
2186
|
queueConsumers.set(key, {
|
|
1841
2187
|
queue: key,
|
|
@@ -1971,6 +2317,426 @@ function isLazyComponentRef(ref) {
|
|
|
1971
2317
|
return typeof ref === "object" && ref !== null && "__rebaseLazy" in ref && ref.__rebaseLazy === true;
|
|
1972
2318
|
}
|
|
1973
2319
|
//#endregion
|
|
2320
|
+
//#region src/types/auth_adapter.ts
|
|
2321
|
+
/**
|
|
2322
|
+
* The request header that asks for a data-plane request to run as another
|
|
2323
|
+
* user, named by uid.
|
|
2324
|
+
*
|
|
2325
|
+
* The server honours it only on a signed-in administrator's own session —
|
|
2326
|
+
* never on an API key or the service key — and only through an adapter that
|
|
2327
|
+
* implements {@link AuthAdapter.resolveUser}. Everywhere else a request that
|
|
2328
|
+
* carries it is refused, never run as its caller.
|
|
2329
|
+
*
|
|
2330
|
+
* @group Auth
|
|
2331
|
+
*/
|
|
2332
|
+
var IMPERSONATE_HEADER = "x-rebase-impersonate";
|
|
2333
|
+
//#endregion
|
|
2334
|
+
//#region src/types/scopes.ts
|
|
2335
|
+
/**
|
|
2336
|
+
* Scopes — what a credential may do, named `resource:action`.
|
|
2337
|
+
*
|
|
2338
|
+
* One vocabulary for every way of reaching a Rebase backend: a person's
|
|
2339
|
+
* session, a service key, a personal key and an OAuth token for `/mcp` all
|
|
2340
|
+
* answer "may this caller do X?" with the same strings, so a grant reads the
|
|
2341
|
+
* same on a key, on a role and on a consent screen.
|
|
2342
|
+
*
|
|
2343
|
+
* A scope may carry a target — `data:read:posts` is `data:read` on the `posts`
|
|
2344
|
+
* collection only. The unqualified scope covers every target. Only the data
|
|
2345
|
+
* plane and app scopes take targets; see {@link SCOPE_TARGETS}.
|
|
2346
|
+
*
|
|
2347
|
+
* Two planes, held differently:
|
|
2348
|
+
*
|
|
2349
|
+
* - **The data plane** (`data:*`, `storage:*`, `functions:invoke`). Every
|
|
2350
|
+
* signed-in person holds all of it, because what a person may do with data
|
|
2351
|
+
* is decided row by row by the collection's `securityRules` and by storage
|
|
2352
|
+
* policies, never by a scope. On a key or a token these scopes narrow: a key
|
|
2353
|
+
* holding `data:read:posts` reaches nothing else, whatever RLS would allow.
|
|
2354
|
+
* - **The admin plane** (everything else). Nobody holds it implicitly. The
|
|
2355
|
+
* `admin` role holds all of it; any other role holds what the app declares
|
|
2356
|
+
* for it under `auth.roles` on the users collection.
|
|
2357
|
+
*
|
|
2358
|
+
* Apps declare their own scopes too (`auth.scopes`), named the same way —
|
|
2359
|
+
* `project:deploy`. Every signed-in person holds every app scope: like the data
|
|
2360
|
+
* plane, the function behind it decides whether the person may act, and the
|
|
2361
|
+
* scope exists so a key can be narrowed to that one action.
|
|
2362
|
+
*
|
|
2363
|
+
* @module
|
|
2364
|
+
*/
|
|
2365
|
+
/** @group Auth */
|
|
2366
|
+
var DATA_PLANE_SCOPES = [
|
|
2367
|
+
"data:read",
|
|
2368
|
+
"data:write",
|
|
2369
|
+
"data:delete",
|
|
2370
|
+
"storage:read",
|
|
2371
|
+
"storage:write",
|
|
2372
|
+
"storage:delete",
|
|
2373
|
+
"functions:invoke"
|
|
2374
|
+
];
|
|
2375
|
+
/** @group Auth */
|
|
2376
|
+
var ADMIN_SCOPES = [
|
|
2377
|
+
"users:read",
|
|
2378
|
+
"users:write",
|
|
2379
|
+
"schema:read",
|
|
2380
|
+
"schema:write",
|
|
2381
|
+
"database:read",
|
|
2382
|
+
"database:write",
|
|
2383
|
+
"backups:read",
|
|
2384
|
+
"cron:read",
|
|
2385
|
+
"cron:write",
|
|
2386
|
+
"logs:read",
|
|
2387
|
+
"keys:read",
|
|
2388
|
+
"keys:write"
|
|
2389
|
+
];
|
|
2390
|
+
/** Every scope Rebase itself defines, in display order. @group Auth */
|
|
2391
|
+
var BUILT_IN_SCOPES = [...DATA_PLANE_SCOPES, ...ADMIN_SCOPES];
|
|
2392
|
+
/**
|
|
2393
|
+
* Scopes no key may hold.
|
|
2394
|
+
*
|
|
2395
|
+
* A key that could manage keys could mint its own successor, widen itself, or
|
|
2396
|
+
* revoke the keys other integrations run on — and revoking it would undo none
|
|
2397
|
+
* of that. Managing keys is for a person (or the service key).
|
|
2398
|
+
*
|
|
2399
|
+
* @group Auth
|
|
2400
|
+
*/
|
|
2401
|
+
var KEY_MANAGEMENT_SCOPES = ["keys:read", "keys:write"];
|
|
2402
|
+
/** The one built-in role. It holds every scope and is the RLS `admin` role. @group Auth */
|
|
2403
|
+
var ADMIN_ROLE = "admin";
|
|
2404
|
+
/**
|
|
2405
|
+
* The built-in scopes that accept a target, and what the target names.
|
|
2406
|
+
*
|
|
2407
|
+
* - `data:*` — a collection slug
|
|
2408
|
+
* - `storage:*` — a storage source id (`(default)` for the default source)
|
|
2409
|
+
* - `functions:invoke` — a custom function's name
|
|
2410
|
+
*
|
|
2411
|
+
* @group Auth
|
|
2412
|
+
*/
|
|
2413
|
+
var SCOPE_TARGETS = {
|
|
2414
|
+
"data:read": "collection",
|
|
2415
|
+
"data:write": "collection",
|
|
2416
|
+
"data:delete": "collection",
|
|
2417
|
+
"storage:read": "bucket",
|
|
2418
|
+
"storage:write": "bucket",
|
|
2419
|
+
"storage:delete": "bucket",
|
|
2420
|
+
"functions:invoke": "function"
|
|
2421
|
+
};
|
|
2422
|
+
/** @group Auth */
|
|
2423
|
+
var BUILT_IN_SCOPE_DESCRIPTIONS = {
|
|
2424
|
+
"data:read": {
|
|
2425
|
+
label: "Read data",
|
|
2426
|
+
description: "Read rows from collections, through the caller's row-level security."
|
|
2427
|
+
},
|
|
2428
|
+
"data:write": {
|
|
2429
|
+
label: "Create and update data",
|
|
2430
|
+
description: "Create and update rows in collections, through the caller's row-level security."
|
|
2431
|
+
},
|
|
2432
|
+
"data:delete": {
|
|
2433
|
+
label: "Delete data",
|
|
2434
|
+
description: "Delete rows from collections, through the caller's row-level security."
|
|
2435
|
+
},
|
|
2436
|
+
"storage:read": {
|
|
2437
|
+
label: "Read files",
|
|
2438
|
+
description: "List and download stored files."
|
|
2439
|
+
},
|
|
2440
|
+
"storage:write": {
|
|
2441
|
+
label: "Upload files",
|
|
2442
|
+
description: "Upload files and create folders."
|
|
2443
|
+
},
|
|
2444
|
+
"storage:delete": {
|
|
2445
|
+
label: "Delete files",
|
|
2446
|
+
description: "Delete stored files."
|
|
2447
|
+
},
|
|
2448
|
+
"functions:invoke": {
|
|
2449
|
+
label: "Call functions",
|
|
2450
|
+
description: "Call the backend's custom functions. A function can do anything its code does."
|
|
2451
|
+
},
|
|
2452
|
+
"users:read": {
|
|
2453
|
+
label: "See users",
|
|
2454
|
+
description: "List user accounts and their roles."
|
|
2455
|
+
},
|
|
2456
|
+
"users:write": {
|
|
2457
|
+
label: "Manage users",
|
|
2458
|
+
description: "Create, edit and delete accounts, reset passwords and second factors, and assign roles up to the holder's own."
|
|
2459
|
+
},
|
|
2460
|
+
"schema:read": {
|
|
2461
|
+
label: "See the schema",
|
|
2462
|
+
description: "Read the collection schema, plan schema changes and run the RLS audit."
|
|
2463
|
+
},
|
|
2464
|
+
"schema:write": {
|
|
2465
|
+
label: "Change the schema",
|
|
2466
|
+
description: "Apply schema changes: edits collection files and alters the database."
|
|
2467
|
+
},
|
|
2468
|
+
"database:read": {
|
|
2469
|
+
label: "Inspect the database",
|
|
2470
|
+
description: "List databases, tables, Postgres roles and branches."
|
|
2471
|
+
},
|
|
2472
|
+
"database:write": {
|
|
2473
|
+
label: "Run SQL",
|
|
2474
|
+
description: "Run SQL as the database owner, outside row-level security, and create or delete branches."
|
|
2475
|
+
},
|
|
2476
|
+
"backups:read": {
|
|
2477
|
+
label: "Download backups",
|
|
2478
|
+
description: "List and download database backups — every row, outside row-level security."
|
|
2479
|
+
},
|
|
2480
|
+
"cron:read": {
|
|
2481
|
+
label: "See cron jobs",
|
|
2482
|
+
description: "List cron jobs and read their run history."
|
|
2483
|
+
},
|
|
2484
|
+
"cron:write": {
|
|
2485
|
+
label: "Run cron jobs",
|
|
2486
|
+
description: "Trigger cron jobs and switch them on or off."
|
|
2487
|
+
},
|
|
2488
|
+
"logs:read": {
|
|
2489
|
+
label: "Read server logs",
|
|
2490
|
+
description: "Read the server's request and application logs."
|
|
2491
|
+
},
|
|
2492
|
+
"keys:read": {
|
|
2493
|
+
label: "See service keys",
|
|
2494
|
+
description: "List the project's service keys. Never grantable to a key."
|
|
2495
|
+
},
|
|
2496
|
+
"keys:write": {
|
|
2497
|
+
label: "Manage service keys",
|
|
2498
|
+
description: "Create, change and revoke the project's service keys. Never grantable to a key."
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2501
|
+
/** The access model of an app that declares nothing: built-ins only. @group Auth */
|
|
2502
|
+
var EMPTY_ACCESS_MODEL = {
|
|
2503
|
+
roles: {},
|
|
2504
|
+
scopes: {}
|
|
2505
|
+
};
|
|
2506
|
+
var SCOPE_PART = /^[a-z][a-z0-9-]*$/;
|
|
2507
|
+
/**
|
|
2508
|
+
* Split `resource:action[:target]`. Returns null for anything else.
|
|
2509
|
+
*
|
|
2510
|
+
* The target is everything after the second colon, so a target may itself
|
|
2511
|
+
* contain colons. Resource and action are lower-case words.
|
|
2512
|
+
*
|
|
2513
|
+
* @group Auth
|
|
2514
|
+
*/
|
|
2515
|
+
function parseScope(value) {
|
|
2516
|
+
if (typeof value !== "string") return null;
|
|
2517
|
+
const first = value.indexOf(":");
|
|
2518
|
+
if (first < 0) return null;
|
|
2519
|
+
const second = value.indexOf(":", first + 1);
|
|
2520
|
+
const resource = value.slice(0, first);
|
|
2521
|
+
const action = second < 0 ? value.slice(first + 1) : value.slice(first + 1, second);
|
|
2522
|
+
if (!SCOPE_PART.test(resource) || !SCOPE_PART.test(action)) return null;
|
|
2523
|
+
if (second < 0) return { scope: `${resource}:${action}` };
|
|
2524
|
+
const target = value.slice(second + 1);
|
|
2525
|
+
if (target.length === 0 || /\s/.test(target)) return null;
|
|
2526
|
+
return {
|
|
2527
|
+
scope: `${resource}:${action}`,
|
|
2528
|
+
target
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
/** @group Auth */
|
|
2532
|
+
function isBuiltInScope(scope) {
|
|
2533
|
+
return BUILT_IN_SCOPES.includes(scope);
|
|
2534
|
+
}
|
|
2535
|
+
/** @group Auth */
|
|
2536
|
+
function isDataPlaneScope(scope) {
|
|
2537
|
+
return DATA_PLANE_SCOPES.includes(scope);
|
|
2538
|
+
}
|
|
2539
|
+
/** @group Auth */
|
|
2540
|
+
function isAdminScope(scope) {
|
|
2541
|
+
return ADMIN_SCOPES.includes(scope);
|
|
2542
|
+
}
|
|
2543
|
+
/** Does this list of roles include the built-in `admin` role? @group Auth */
|
|
2544
|
+
function hasAdminRole(roles) {
|
|
2545
|
+
return !!roles?.includes(ADMIN_ROLE);
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Does a set of held scopes grant `scope`, optionally on one `target`?
|
|
2549
|
+
*
|
|
2550
|
+
* The unqualified grant covers every target. A qualified grant covers its own
|
|
2551
|
+
* target only, and never answers an unqualified question: a key holding
|
|
2552
|
+
* `data:read:posts` is not asked "may you read data?" and told yes.
|
|
2553
|
+
*
|
|
2554
|
+
* @group Auth
|
|
2555
|
+
*/
|
|
2556
|
+
function scopeGrants(held, scope, target) {
|
|
2557
|
+
for (const entry of held) {
|
|
2558
|
+
if (entry === scope) return true;
|
|
2559
|
+
if (target !== void 0 && entry === `${scope}:${target}`) return true;
|
|
2560
|
+
}
|
|
2561
|
+
return false;
|
|
2562
|
+
}
|
|
2563
|
+
/**
|
|
2564
|
+
* Does a set of held scopes grant `scope` on at least one target?
|
|
2565
|
+
*
|
|
2566
|
+
* For a surface that lists what is reachable — the collections an MCP client
|
|
2567
|
+
* may query — and filters per item afterwards.
|
|
2568
|
+
*
|
|
2569
|
+
* @group Auth
|
|
2570
|
+
*/
|
|
2571
|
+
function scopeGrantsAny(held, scope) {
|
|
2572
|
+
const prefix = `${scope}:`;
|
|
2573
|
+
return held.some((entry) => entry === scope || entry.startsWith(prefix));
|
|
2574
|
+
}
|
|
2575
|
+
/**
|
|
2576
|
+
* The targets a held set narrows `scope` to: `"all"` when it holds the
|
|
2577
|
+
* unqualified scope, otherwise the listed targets (possibly none).
|
|
2578
|
+
*
|
|
2579
|
+
* @group Auth
|
|
2580
|
+
*/
|
|
2581
|
+
function scopeTargets(held, scope) {
|
|
2582
|
+
const prefix = `${scope}:`;
|
|
2583
|
+
const targets = [];
|
|
2584
|
+
for (const entry of held) {
|
|
2585
|
+
if (entry === scope) return "all";
|
|
2586
|
+
if (entry.startsWith(prefix)) targets.push(entry.slice(prefix.length));
|
|
2587
|
+
}
|
|
2588
|
+
return targets;
|
|
2589
|
+
}
|
|
2590
|
+
/**
|
|
2591
|
+
* Every scope a person holding `roles` has.
|
|
2592
|
+
*
|
|
2593
|
+
* The data plane and every app scope always; the whole admin plane with the
|
|
2594
|
+
* `admin` role; otherwise each declared role's scopes. A role nobody declared
|
|
2595
|
+
* adds nothing — it may still mean something to an RLS policy.
|
|
2596
|
+
*
|
|
2597
|
+
* @group Auth
|
|
2598
|
+
*/
|
|
2599
|
+
function scopesForRoles(roles, model = EMPTY_ACCESS_MODEL) {
|
|
2600
|
+
const held = /* @__PURE__ */ new Set([...DATA_PLANE_SCOPES, ...Object.keys(model.scopes)]);
|
|
2601
|
+
if (hasAdminRole(roles)) {
|
|
2602
|
+
for (const scope of ADMIN_SCOPES) held.add(scope);
|
|
2603
|
+
return [...held];
|
|
2604
|
+
}
|
|
2605
|
+
for (const role of roles ?? []) {
|
|
2606
|
+
const definition = Object.prototype.hasOwnProperty.call(model.roles, role) ? model.roles[role] : void 0;
|
|
2607
|
+
for (const scope of definition?.scopes ?? []) held.add(scope);
|
|
2608
|
+
}
|
|
2609
|
+
return [...held];
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Every scope this access model knows, unqualified: the built-ins, then the
|
|
2613
|
+
* app's own.
|
|
2614
|
+
*
|
|
2615
|
+
* @group Auth
|
|
2616
|
+
*/
|
|
2617
|
+
function knownScopes(model = EMPTY_ACCESS_MODEL) {
|
|
2618
|
+
return [...BUILT_IN_SCOPES, ...Object.keys(model.scopes)];
|
|
2619
|
+
}
|
|
2620
|
+
/** Does this scope accept a target? @group Auth */
|
|
2621
|
+
function scopeAcceptsTarget(scope, model = EMPTY_ACCESS_MODEL) {
|
|
2622
|
+
if (isBuiltInScope(scope)) return SCOPE_TARGETS[scope] !== void 0;
|
|
2623
|
+
return (Object.prototype.hasOwnProperty.call(model.scopes, scope) ? model.scopes[scope] : void 0)?.target !== void 0;
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Check a list of scope strings against an access model. Returns the
|
|
2627
|
+
* problems, empty when every entry is a known scope with an acceptable target.
|
|
2628
|
+
*
|
|
2629
|
+
* @group Auth
|
|
2630
|
+
*/
|
|
2631
|
+
function validateScopes(values, model = EMPTY_ACCESS_MODEL) {
|
|
2632
|
+
const known = new Set(knownScopes(model));
|
|
2633
|
+
const problems = [];
|
|
2634
|
+
for (const value of values) {
|
|
2635
|
+
const text = typeof value === "string" ? value : String(value);
|
|
2636
|
+
const parsed = typeof value === "string" ? parseScope(value) : null;
|
|
2637
|
+
if (!parsed) problems.push({
|
|
2638
|
+
scope: text,
|
|
2639
|
+
reason: "malformed"
|
|
2640
|
+
});
|
|
2641
|
+
else if (!known.has(parsed.scope)) problems.push({
|
|
2642
|
+
scope: text,
|
|
2643
|
+
reason: "unknown"
|
|
2644
|
+
});
|
|
2645
|
+
else if (parsed.target !== void 0 && !scopeAcceptsTarget(parsed.scope, model)) problems.push({
|
|
2646
|
+
scope: text,
|
|
2647
|
+
reason: "target-not-accepted"
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
return problems;
|
|
2651
|
+
}
|
|
2652
|
+
/**
|
|
2653
|
+
* The entries of `requested` that `held` does not cover — what a credential
|
|
2654
|
+
* would hold beyond its minter. Empty means the request is within reach.
|
|
2655
|
+
*
|
|
2656
|
+
* `data:read:posts` is covered by `data:read`; `data:read` is not covered by
|
|
2657
|
+
* `data:read:posts`.
|
|
2658
|
+
*
|
|
2659
|
+
* @group Auth
|
|
2660
|
+
*/
|
|
2661
|
+
function scopesBeyond(requested, held) {
|
|
2662
|
+
return requested.filter((entry) => {
|
|
2663
|
+
const parsed = parseScope(entry);
|
|
2664
|
+
if (!parsed) return true;
|
|
2665
|
+
return !scopeGrants(held, parsed.scope, parsed.target);
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Narrow `requested` to what `held` covers, entry by entry. What a personal
|
|
2670
|
+
* key holds today when its owner has lost a role since it was minted.
|
|
2671
|
+
*
|
|
2672
|
+
* @group Auth
|
|
2673
|
+
*/
|
|
2674
|
+
function intersectScopes(requested, held) {
|
|
2675
|
+
const beyond = new Set(scopesBeyond(requested, held));
|
|
2676
|
+
return requested.filter((entry) => !beyond.has(entry));
|
|
2677
|
+
}
|
|
2678
|
+
/**
|
|
2679
|
+
* How a scope is worded for a person: the built-in description, the app's
|
|
2680
|
+
* declared label, or the raw string when nothing describes it. A target is
|
|
2681
|
+
* named after the label.
|
|
2682
|
+
*
|
|
2683
|
+
* @group Auth
|
|
2684
|
+
*/
|
|
2685
|
+
function describeScope(value, model = EMPTY_ACCESS_MODEL) {
|
|
2686
|
+
const parsed = parseScope(value);
|
|
2687
|
+
if (!parsed) return {
|
|
2688
|
+
label: value,
|
|
2689
|
+
description: ""
|
|
2690
|
+
};
|
|
2691
|
+
let base;
|
|
2692
|
+
if (isBuiltInScope(parsed.scope)) base = BUILT_IN_SCOPE_DESCRIPTIONS[parsed.scope];
|
|
2693
|
+
else if (Object.prototype.hasOwnProperty.call(model.scopes, parsed.scope)) {
|
|
2694
|
+
const definition = model.scopes[parsed.scope];
|
|
2695
|
+
base = {
|
|
2696
|
+
label: definition.label,
|
|
2697
|
+
description: definition.description ?? ""
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
const label = base?.label ?? parsed.scope;
|
|
2701
|
+
return {
|
|
2702
|
+
label: parsed.target !== void 0 ? `${label}: ${parsed.target}` : label,
|
|
2703
|
+
description: base?.description ?? ""
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
/** The roles of an access model, `admin` first. @group Auth */
|
|
2707
|
+
function summarizeRoles(model = EMPTY_ACCESS_MODEL) {
|
|
2708
|
+
return [{
|
|
2709
|
+
id: ADMIN_ROLE,
|
|
2710
|
+
name: "Admin",
|
|
2711
|
+
description: "Holds every scope. Row-level security does not limit it: it reads, changes and deletes every row.",
|
|
2712
|
+
scopes: [...ADMIN_SCOPES],
|
|
2713
|
+
builtIn: true
|
|
2714
|
+
}, ...Object.entries(model.roles).map(([id, definition]) => ({
|
|
2715
|
+
id,
|
|
2716
|
+
name: definition.name ?? id,
|
|
2717
|
+
...definition.description ? { description: definition.description } : {},
|
|
2718
|
+
scopes: [...definition.scopes],
|
|
2719
|
+
builtIn: false
|
|
2720
|
+
}))];
|
|
2721
|
+
}
|
|
2722
|
+
/** Every scope of an access model, described, in display order. @group Auth */
|
|
2723
|
+
function summarizeScopes(model = EMPTY_ACCESS_MODEL) {
|
|
2724
|
+
const builtIn = BUILT_IN_SCOPES.map((scope) => ({
|
|
2725
|
+
scope,
|
|
2726
|
+
...BUILT_IN_SCOPE_DESCRIPTIONS[scope],
|
|
2727
|
+
...SCOPE_TARGETS[scope] ? { target: SCOPE_TARGETS[scope] } : {},
|
|
2728
|
+
plane: isDataPlaneScope(scope) ? "data" : "admin"
|
|
2729
|
+
}));
|
|
2730
|
+
const app = Object.entries(model.scopes).map(([scope, definition]) => ({
|
|
2731
|
+
scope,
|
|
2732
|
+
label: definition.label,
|
|
2733
|
+
description: definition.description ?? "",
|
|
2734
|
+
...definition.target ? { target: definition.target } : {},
|
|
2735
|
+
plane: "app"
|
|
2736
|
+
}));
|
|
2737
|
+
return [...builtIn, ...app];
|
|
2738
|
+
}
|
|
2739
|
+
//#endregion
|
|
1974
2740
|
//#region src/types/project_manifest.ts
|
|
1975
2741
|
/**
|
|
1976
2742
|
* Path prefixes the backend owns, which no static app may claim.
|
|
@@ -2008,6 +2774,91 @@ function reservedPrefixFor(path) {
|
|
|
2008
2774
|
return RESERVED_BACKEND_PREFIXES.find((reserved) => normalized === reserved || normalized.startsWith(`${reserved}/`));
|
|
2009
2775
|
}
|
|
2010
2776
|
/**
|
|
2777
|
+
* Split a static app's declared `path` into the hostname it claims and the path
|
|
2778
|
+
* under it.
|
|
2779
|
+
*
|
|
2780
|
+
* `"/admin"` is `{ path: "/admin" }`, returned as written: the path rules
|
|
2781
|
+
* (leading slash, no `..`, reserved prefixes) belong to the caller, which
|
|
2782
|
+
* applies them to the path part of either form. `"https://admin.example.com/cms"`
|
|
2783
|
+
* is `{ host: "admin.example.com", path: "/cms" }`.
|
|
2784
|
+
*
|
|
2785
|
+
* Shared because three parties must read the same value the same way: the CLI
|
|
2786
|
+
* while `rebase.json` is edited, the control plane at deploy intake (a bundle can
|
|
2787
|
+
* come from any CLI, or none), and the runtime choosing which app answers.
|
|
2788
|
+
*/
|
|
2789
|
+
function parseAppAddress(value) {
|
|
2790
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
|
|
2791
|
+
if (!value.startsWith("/") && isValidAppHost(value.toLowerCase().replace(/\/.*$/, ""))) return {
|
|
2792
|
+
ok: false,
|
|
2793
|
+
reason: `looks like a hostname — write it as a URL: "https://${value}"`
|
|
2794
|
+
};
|
|
2795
|
+
return {
|
|
2796
|
+
ok: true,
|
|
2797
|
+
address: { path: value }
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
if (/[\s\\\u0000-\u001f\u007f]/.test(value)) return {
|
|
2801
|
+
ok: false,
|
|
2802
|
+
reason: "must not contain spaces, control characters or backslashes"
|
|
2803
|
+
};
|
|
2804
|
+
const authority = value.slice(value.indexOf("//") + 2).split(/[/?#]/)[0];
|
|
2805
|
+
if (/:[^@]*$/.test(authority) && !authority.startsWith("[")) return {
|
|
2806
|
+
ok: false,
|
|
2807
|
+
reason: "must not name a port — a hostname is served on 443"
|
|
2808
|
+
};
|
|
2809
|
+
let url;
|
|
2810
|
+
try {
|
|
2811
|
+
url = new URL(value);
|
|
2812
|
+
} catch {
|
|
2813
|
+
return {
|
|
2814
|
+
ok: false,
|
|
2815
|
+
reason: "is not a valid URL"
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
if (url.protocol !== "https:") return {
|
|
2819
|
+
ok: false,
|
|
2820
|
+
reason: `must use https — the platform serves every hostname over TLS: "https://${url.host}${url.pathname === "/" ? "" : url.pathname}"`
|
|
2821
|
+
};
|
|
2822
|
+
if (url.username || url.password) return {
|
|
2823
|
+
ok: false,
|
|
2824
|
+
reason: "must not contain credentials"
|
|
2825
|
+
};
|
|
2826
|
+
if (url.port) return {
|
|
2827
|
+
ok: false,
|
|
2828
|
+
reason: "must not name a port — a hostname is served on 443"
|
|
2829
|
+
};
|
|
2830
|
+
if (url.search || url.hash || /[?#]/.test(value)) return {
|
|
2831
|
+
ok: false,
|
|
2832
|
+
reason: "must not have a query or fragment"
|
|
2833
|
+
};
|
|
2834
|
+
if (!isValidAppHost(url.hostname)) return {
|
|
2835
|
+
ok: false,
|
|
2836
|
+
reason: `names "${url.hostname}", which is not a public hostname`
|
|
2837
|
+
};
|
|
2838
|
+
const authorityAndPath = value.slice(value.indexOf("//") + 2);
|
|
2839
|
+
const slash = authorityAndPath.indexOf("/");
|
|
2840
|
+
return {
|
|
2841
|
+
ok: true,
|
|
2842
|
+
address: {
|
|
2843
|
+
host: url.hostname,
|
|
2844
|
+
path: slash === -1 ? "/" : authorityAndPath.slice(slash)
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* Whether `host` is a public DNS name an app can be given: lowercase labels of
|
|
2850
|
+
* letters, digits and hyphens, at least two of them, not an IP address and not
|
|
2851
|
+
* `localhost`. Internationalised names arrive here already in punycode.
|
|
2852
|
+
*/
|
|
2853
|
+
function isValidAppHost(host) {
|
|
2854
|
+
if (host.length === 0 || host.length > 253 || host !== host.toLowerCase()) return false;
|
|
2855
|
+
const labels = host.split(".");
|
|
2856
|
+
if (labels.length < 2) return false;
|
|
2857
|
+
const label = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
2858
|
+
if (!labels.every((part) => label.test(part))) return false;
|
|
2859
|
+
return !/^[0-9]+$/.test(labels[labels.length - 1]);
|
|
2860
|
+
}
|
|
2861
|
+
/**
|
|
2011
2862
|
* Version of the bundle *format* itself.
|
|
2012
2863
|
*
|
|
2013
2864
|
* Bumped only when the on-disk layout changes in a way an older runtime could
|
|
@@ -2458,9 +3309,33 @@ function resolveClientListLimit(rawLimit, opts = {}) {
|
|
|
2458
3309
|
*/
|
|
2459
3310
|
var PUBLIC_STORAGE_PREFIX = "public/";
|
|
2460
3311
|
/**
|
|
2461
|
-
* True when a storage key
|
|
2462
|
-
* {@link PUBLIC_STORAGE_PREFIX}
|
|
2463
|
-
*
|
|
3312
|
+
* True when a storage **key** is public: it lies under
|
|
3313
|
+
* {@link PUBLIC_STORAGE_PREFIX}.
|
|
3314
|
+
*
|
|
3315
|
+
* The key is read exactly as it is — the canonical key within its bucket, the
|
|
3316
|
+
* one the server stores, serves, and hands a `storageAuthorize` hook as
|
|
3317
|
+
* `key` — and nothing is parsed out of it. That is the difference from
|
|
3318
|
+
* {@link isPublicStoragePath}, which reads a client-side *path* and so strips
|
|
3319
|
+
* a `scheme://` and a leading `default/` bucket segment first. On a key both are
|
|
3320
|
+
* wrong: `default/public/x` is a key in a folder called `default`, and a
|
|
3321
|
+
* request path `notes://public/x` names the key `notes:/public/x`. Neither is
|
|
3322
|
+
* public, and the server decides on this function so that it never says they
|
|
3323
|
+
* are.
|
|
3324
|
+
*
|
|
3325
|
+
* @group Models
|
|
3326
|
+
*/
|
|
3327
|
+
function isPublicStorageKey(key) {
|
|
3328
|
+
if (!key || !key.startsWith("public/")) return false;
|
|
3329
|
+
return !key.split(/[\\/]/).includes("..");
|
|
3330
|
+
}
|
|
3331
|
+
/**
|
|
3332
|
+
* True when a storage key or client-side path points at a public object
|
|
3333
|
+
* (lives under {@link PUBLIC_STORAGE_PREFIX}).
|
|
3334
|
+
*
|
|
3335
|
+
* For a path as a client holds it: a `scheme://` prefix and a single leading
|
|
3336
|
+
* `default/` bucket segment are stripped first. The server and a
|
|
3337
|
+
* `storageAuthorize` hook, which hold the canonical key, use
|
|
3338
|
+
* {@link isPublicStorageKey}.
|
|
2464
3339
|
*
|
|
2465
3340
|
* @group Models
|
|
2466
3341
|
*/
|
|
@@ -2473,7 +3348,189 @@ function isPublicStoragePath(path) {
|
|
|
2473
3348
|
if (p.split("/").some((seg) => seg === "..")) return false;
|
|
2474
3349
|
return p.startsWith("public/") || p.startsWith(`default/public/`);
|
|
2475
3350
|
}
|
|
3351
|
+
/**
|
|
3352
|
+
* The path the storage routes address an object by, from a key or from the
|
|
3353
|
+
* `storageUrl` an upload returned (`<scheme>://<bucket>/<key>`, which is what a
|
|
3354
|
+
* property with `includeBucketUrl: true` keeps in its column).
|
|
3355
|
+
*
|
|
3356
|
+
* The routes address a source's objects by key. On S3 and GCS the bucket in a
|
|
3357
|
+
* `storageUrl` is always the source's own — the server refuses any other — so
|
|
3358
|
+
* it is dropped: sent along, `s3://acme-media/products/a.png` reached the
|
|
3359
|
+
* server as the key `acme-media/products/a.png`, the preview found nothing and
|
|
3360
|
+
* a delete reported success while the object stayed. It worked on local disk
|
|
3361
|
+
* only, because `local://default/<key>` happens to carry the one bucket
|
|
3362
|
+
* segment the routes recognise — which is kept, as is any other local bucket.
|
|
3363
|
+
*
|
|
3364
|
+
* A bare key whose first folder is `default` is sent behind that bucket
|
|
3365
|
+
* segment. The routes read one leading `default/` (in any case) as the bucket,
|
|
3366
|
+
* so the key `default/photo.png` sent as it is names the root `photo.png`:
|
|
3367
|
+
* {@link defaultBucketPath} is the spelling that names the key itself.
|
|
3368
|
+
*
|
|
3369
|
+
* Shared by every storage source that talks to those routes — the SDK's and
|
|
3370
|
+
* the admin panel's `useBackendStorageSource` — so they address an object alike.
|
|
3371
|
+
*
|
|
3372
|
+
* @group Models
|
|
3373
|
+
*/
|
|
3374
|
+
function storageObjectPath(keyOrUrl, bucket) {
|
|
3375
|
+
let filePath = keyOrUrl;
|
|
3376
|
+
let bucketQualified = false;
|
|
3377
|
+
const scheme = /^(local|s3|gs):\/\//.exec(filePath);
|
|
3378
|
+
if (scheme) {
|
|
3379
|
+
filePath = filePath.substring(scheme[0].length);
|
|
3380
|
+
if (scheme[1] !== "local") {
|
|
3381
|
+
const slash = filePath.indexOf("/");
|
|
3382
|
+
if (slash > 0) filePath = filePath.substring(slash + 1);
|
|
3383
|
+
} else bucketQualified = true;
|
|
3384
|
+
}
|
|
3385
|
+
if (bucket && filePath) {
|
|
3386
|
+
if (!filePath.startsWith(`${bucket}/`)) filePath = `${bucket}/${filePath}`;
|
|
3387
|
+
bucketQualified = true;
|
|
3388
|
+
}
|
|
3389
|
+
return bucketQualified ? filePath : defaultBucketPath(filePath);
|
|
3390
|
+
}
|
|
3391
|
+
/**
|
|
3392
|
+
* The request path of a key in the default bucket: the key itself, unless the
|
|
3393
|
+
* routes would read its first folder as the bucket — a first folder named
|
|
3394
|
+
* `default`, matched as the routes match it (case-insensitively, and only when
|
|
3395
|
+
* something follows it). Then the bucket segment goes in front, so exactly the
|
|
3396
|
+
* one segment the routes strip is the one added.
|
|
3397
|
+
*/
|
|
3398
|
+
function defaultBucketPath(key) {
|
|
3399
|
+
const segments = key.split("/");
|
|
3400
|
+
return segments.length > 1 && segments[0].toLowerCase() === "default" ? `default/${key}` : key;
|
|
3401
|
+
}
|
|
3402
|
+
/**
|
|
3403
|
+
* The scheme of a **storage reference**: a stored object named inside text.
|
|
3404
|
+
*
|
|
3405
|
+
* Text that embeds a file — a markdown body above all — cannot hold a private
|
|
3406
|
+
* object's URL: that URL carries a download token which expires in minutes,
|
|
3407
|
+
* after which every reader of the text gets a 401 for an image that is still in
|
|
3408
|
+
* the bucket. It holds a reference instead,
|
|
3409
|
+
* `rebase-storage:posts/cover.png` (`?storageId=media` for a named source),
|
|
3410
|
+
* and whoever renders the text exchanges each reference for a fresh URL with
|
|
3411
|
+
* {@link resolveStorageReferences} — `resolveStorageReferences(text, client)`
|
|
3412
|
+
* from `@rebasepro/client` on a site.
|
|
3413
|
+
*
|
|
3414
|
+
* @group Models
|
|
3415
|
+
*/
|
|
3416
|
+
var STORAGE_REFERENCE_SCHEME = "rebase-storage:";
|
|
3417
|
+
/**
|
|
3418
|
+
* `encodeURIComponent`, plus the five characters it leaves alone that end or
|
|
3419
|
+
* break a markdown link destination: `(`, `)`, `'`, `!` and `*`.
|
|
3420
|
+
*/
|
|
3421
|
+
var encodeReferencePart = (part) => encodeURIComponent(part).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
3422
|
+
/**
|
|
3423
|
+
* A reference as it appears in text: the scheme, then the characters
|
|
3424
|
+
* {@link storageReference} writes. Kept beside {@link STORAGE_REFERENCE_SCHEME};
|
|
3425
|
+
* the test round-trips names chosen to break it.
|
|
3426
|
+
*/
|
|
3427
|
+
var STORAGE_REFERENCE_PATTERN = /rebase-storage:[A-Za-z0-9\-._~%/?=&]+/g;
|
|
3428
|
+
/**
|
|
3429
|
+
* The reference for an object — what text stores in place of its URL.
|
|
3430
|
+
*
|
|
3431
|
+
* Every part is percent-encoded, so the reference contains no space,
|
|
3432
|
+
* parenthesis or quote and survives as one markdown link destination whatever
|
|
3433
|
+
* the file was called.
|
|
3434
|
+
*
|
|
3435
|
+
* @group Models
|
|
3436
|
+
*/
|
|
3437
|
+
function storageReference(key, storageId) {
|
|
3438
|
+
return `${STORAGE_REFERENCE_SCHEME}${key.replace(/^\/+/, "").split("/").map(encodeReferencePart).join("/")}${storageId && storageId !== "(default)" ? `?storageId=${encodeReferencePart(storageId)}` : ""}`;
|
|
3439
|
+
}
|
|
3440
|
+
/**
|
|
3441
|
+
* Whether a string — an image's `src`, a link's `href` — is a storage reference.
|
|
3442
|
+
*
|
|
3443
|
+
* @group Models
|
|
3444
|
+
*/
|
|
3445
|
+
function isStorageReference(value) {
|
|
3446
|
+
return typeof value === "string" && value.startsWith("rebase-storage:");
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* The object a storage reference names, or `null` for anything else — a URL, a
|
|
3450
|
+
* bare key, or a reference whose key would climb out of its prefix.
|
|
3451
|
+
*
|
|
3452
|
+
* @group Models
|
|
3453
|
+
*/
|
|
3454
|
+
function parseStorageReference(value) {
|
|
3455
|
+
if (!isStorageReference(value)) return null;
|
|
3456
|
+
const rest = value.slice(15);
|
|
3457
|
+
const queryAt = rest.indexOf("?");
|
|
3458
|
+
const rawPath = queryAt === -1 ? rest : rest.slice(0, queryAt);
|
|
3459
|
+
const query = queryAt === -1 ? "" : rest.slice(queryAt + 1);
|
|
3460
|
+
let key;
|
|
3461
|
+
let storageId;
|
|
3462
|
+
try {
|
|
3463
|
+
key = rawPath.split("/").map(decodeURIComponent).join("/");
|
|
3464
|
+
for (const pair of query.split("&")) {
|
|
3465
|
+
const [name, raw] = pair.split("=");
|
|
3466
|
+
if (name === "storageId" && raw) storageId = decodeURIComponent(raw);
|
|
3467
|
+
}
|
|
3468
|
+
} catch {
|
|
3469
|
+
return null;
|
|
3470
|
+
}
|
|
3471
|
+
if (!key || key.split("/").some((segment) => segment === "..")) return null;
|
|
3472
|
+
return storageId && storageId !== "(default)" ? {
|
|
3473
|
+
key,
|
|
3474
|
+
storageId
|
|
3475
|
+
} : { key };
|
|
3476
|
+
}
|
|
3477
|
+
/**
|
|
3478
|
+
* Exchange every storage reference in `text` for a URL, at the moment the text
|
|
3479
|
+
* is rendered.
|
|
3480
|
+
*
|
|
3481
|
+
* ```ts
|
|
3482
|
+
* const html = markdown.render(await resolveStorageReferences(post.body, client));
|
|
3483
|
+
* ```
|
|
3484
|
+
*
|
|
3485
|
+
* Each distinct object is asked for once — `getSignedUrl(key)` on its source —
|
|
3486
|
+
* so a private object gets a freshly minted download URL every time the text is
|
|
3487
|
+
* rendered. A reference that cannot be resolved (no such object, an unknown
|
|
3488
|
+
* source) is left as it was, so a missing object reads as a missing image
|
|
3489
|
+
* rather than as a failed render.
|
|
3490
|
+
*
|
|
3491
|
+
* @group Models
|
|
3492
|
+
*/
|
|
3493
|
+
async function resolveStorageReferences(text, from) {
|
|
3494
|
+
if (!text || !text.includes("rebase-storage:")) return text;
|
|
3495
|
+
const resolve = typeof from === "function" ? from : resolverFromSources(from);
|
|
3496
|
+
const references = new Set(text.match(STORAGE_REFERENCE_PATTERN) ?? []);
|
|
3497
|
+
const urls = /* @__PURE__ */ new Map();
|
|
3498
|
+
await Promise.all([...references].map(async (reference) => {
|
|
3499
|
+
const target = parseStorageReference(reference);
|
|
3500
|
+
if (!target) return;
|
|
3501
|
+
try {
|
|
3502
|
+
const url = await resolve(target);
|
|
3503
|
+
if (url) urls.set(reference, url);
|
|
3504
|
+
} catch {}
|
|
3505
|
+
}));
|
|
3506
|
+
if (urls.size === 0) return text;
|
|
3507
|
+
return text.replace(STORAGE_REFERENCE_PATTERN, (reference) => {
|
|
3508
|
+
const url = urls.get(reference);
|
|
3509
|
+
return url === void 0 ? reference : asLinkDestination(url);
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* A URL as one markdown link destination: whitespace, parentheses, angle
|
|
3514
|
+
* brackets and quotes percent-encoded, since any of them ends the destination
|
|
3515
|
+
* and turns the image into literal text. The URL means the same thing either
|
|
3516
|
+
* way.
|
|
3517
|
+
*/
|
|
3518
|
+
var asLinkDestination = (url) => url.replace(/[\s()<>"']/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
3519
|
+
/** A resolver over a client's storage sources, one source per named key. */
|
|
3520
|
+
function resolverFromSources(client) {
|
|
3521
|
+
const named = /* @__PURE__ */ new Map();
|
|
3522
|
+
return async ({ key, storageId }) => {
|
|
3523
|
+
let source = client.storage;
|
|
3524
|
+
if (storageId) {
|
|
3525
|
+
if (!named.has(storageId)) named.set(storageId, client.createStorageSource?.(storageId));
|
|
3526
|
+
source = named.get(storageId);
|
|
3527
|
+
}
|
|
3528
|
+
if (!source) return null;
|
|
3529
|
+
const config = await source.getSignedUrl(key);
|
|
3530
|
+
return config.fileNotFound ? null : config.url;
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
2476
3533
|
//#endregion
|
|
2477
|
-
export { ADMIN_COLLECTION_KEYS, ADMIN_PROPERTY_KEYS, ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, BATCH_REF_KEY, BUNDLE_FORMAT_VERSION, CANONICAL_TO_REST, DEFAULT_CAPABILITIES, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_FUZZY_THRESHOLD, DEFAULT_LIST_LIMIT, DEFAULT_RESOURCE_KEY, DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE, DEFAULT_SEARCH_MODE, DEFAULT_SEARCH_WEIGHT, DEFAULT_STORAGE_SOURCE_KEY, DEFAULT_TENANT_BYPASS_ROLES, DEFAULT_VECTOR_LIST_LIMIT, EntityReference, EntityRelation, FIELD_OPERATORS, FIREBASE_CAPABILITIES, GeoPoint, JUNCTION_PIVOT_KEY, LEGACY_RLS_JWT_SQL, LEGACY_RLS_ROLES_SQL, LEGACY_RLS_SCHEMA, LEGACY_RLS_UID_SQL, LIST_OPS, ListLimitError, MAX_INCLUDE_DEPTH, MAX_LIST_LIMIT, MONGODB_CAPABILITIES, NULL_OPS, POSTGRES_CAPABILITIES, PUBLIC_STORAGE_PREFIX, REBASE_SCHEMA, RELEVANCE_SORT_FIELD, RESERVED_BACKEND_PREFIXES, RESOURCE_GRAPH_VERSION, REST_TO_CANONICAL, RLS_IS_ANONYMOUS_SQL, RLS_JWT_SQL, RLS_ROLES_SQL, RLS_UID_SQL, RUNTIME_CONTRACT_VERSION, RebaseApiError, RebaseClientError, SCHEMA_VERSION_HEADER, Vector, amendResourceKind, bucket, buildResourceGraph, canonicalSchemaPayload, computeSchemaVersion, database, declareCron, declareFunction, declareResource, declaredDataSources, declaredDatabaseExtensions, declaredQueueConsumers, declaredResources, declaredStorageSources, declaredSubscriptions, deserializeCollections, encodeRelationAggregateSort, envBasesForResource, findEnvSuffixCollision, findStorageSuffixCollision, getCollectionDataPath, getDataSourceCapabilities, getDeclaredSubcollections, hasFieldOperation, hasForeignKeyOnTarget, isAnonymousUid, isBranchAdmin, isChannelBusInstance, isDocumentAdmin, isFieldOperation, isFirebaseCollectionConfig, isLazyComponentRef, isManyToMany, isMongoDBCollectionConfig, isPostgresCollectionConfig, isPublicStoragePath, isRelationAggregateSort, isRelationalCollectionConfig, isResourceHandle, isSQLAdmin, isSchemaAdmin, isSchemaEditingAdmin, isSerializedCollectionRef, isTenantClaimSource, isTenantMembershipSource, isToMany, isUnsupported, isValidEngine, nestAdminCollectionKeys, nestAdminKeysOf, nestAdminPropertyKeys, parseEnvBoolean, parseRelationAggregateSort, policy, queue, registerDataSourceCapabilities, registerResourceKind, reservedPrefixFor, resetDeclaredQueueConsumers, resetDeclaredResources, resetDeclaredSubscriptions, resolveClientListLimit, resolveResourceRefs, resourceEnvSuffix, resourceId, resourceKeyOf, resourceKind, resourceKinds, resourceToDataSource, resourceToStorageSource, rewriteLegacyRlsFunctions, serializeCollections, setQueueRuntime, setTopicRuntime, sortKeyToString, storageEnvSuffix, toCanonicalOp, topic, unsupportedMethod, usesLegacyRlsFunctions };
|
|
3534
|
+
export { ADMIN_COLLECTION_KEYS, ADMIN_PROPERTY_KEYS, ADMIN_ROLE, ADMIN_SCOPES, ALL_WHERE_FILTER_OPS, ANONYMOUS_USER_ID, ANONYMOUS_USER_IDS, BATCH_REF_KEY, BUILT_IN_SCOPES, BUILT_IN_SCOPE_DESCRIPTIONS, BUNDLE_FORMAT_VERSION, CANONICAL_TO_REST, DATA_PLANE_SCOPES, DEFAULT_CAPABILITIES, DEFAULT_COMMIT_PATHS, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELATION_KINDS, DEFAULT_FUZZY_THRESHOLD, DEFAULT_LIST_LIMIT, DEFAULT_RESOURCE_KEY, DEFAULT_SEARCH_COLUMN, DEFAULT_SEARCH_LANGUAGE, DEFAULT_SEARCH_MODE, DEFAULT_SEARCH_WEIGHT, DEFAULT_STORAGE_SOURCE_KEY, DEFAULT_TENANT_BYPASS_ROLES, DEFAULT_VECTOR_LIST_LIMIT, EMPTY_ACCESS_MODEL, EntityReference, EntityRelation, FIELD_OPERATORS, FIREBASE_CAPABILITIES, GeoPoint, IMPERSONATE_HEADER, JUNCTION_PIVOT_KEY, KEY_MANAGEMENT_SCOPES, LEGACY_RLS_JWT_SQL, LEGACY_RLS_ROLES_SQL, LEGACY_RLS_SCHEMA, LEGACY_RLS_UID_SQL, LIST_OPS, ListLimitError, MAX_INCLUDE_DEPTH, MAX_LIST_LIMIT, MONGODB_CAPABILITIES, NULL_OPS, POSTGRES_CAPABILITIES, PUBLIC_STORAGE_PREFIX, REBASE_SCHEMA, RELEVANCE_SORT_FIELD, RESERVED_BACKEND_PREFIXES, RESOURCE_GRAPH_VERSION, REST_TO_CANONICAL, RLS_IS_ANONYMOUS_SQL, RLS_JWT_SQL, RLS_ROLES_SQL, RLS_UID_SQL, RUNTIME_CONTRACT_VERSION, RUNTIME_ONLY_COLLECTION_KEYS, RebaseApiError, RebaseClientError, SCHEMA_VERSION_HEADER, SCOPE_TARGETS, STORAGE_REFERENCE_SCHEME, Vector, amendResourceKind, applyCollectionPatch, bucket, buildResourceGraph, canonicalSchemaPayload, collectionPatchProblems, computeSchemaVersion, database, declareCron, declareFunction, declareResource, declaredDataSources, declaredDatabaseExtensions, declaredQueueConsumers, declaredResources, declaredStorageSources, declaredSubscriptions, describeScope, deserializeCollections, diffCollections, diffJsonValues, encodeRelationAggregateSort, envBasesForResource, findEnvSuffixCollision, findStorageSuffixCollision, getCollectionDataPath, getDataSourceCapabilities, getDeclaredSubcollections, hasAdminRole, hasFieldOperation, hasForeignKeyOnTarget, intersectScopes, isAdminScope, isAnonymousUid, isBranchAdmin, isBuiltInScope, isChannelBusInstance, isCollectionPatch, isDataPlaneScope, isDocumentAdmin, isFieldOperation, isFirebaseCollectionConfig, isLazyComponentRef, isManyToMany, isMongoDBCollectionConfig, isPostgresCollectionConfig, isPublicStorageKey, isPublicStoragePath, isRelationAggregateSort, isRelationalCollectionConfig, isResourceHandle, isSQLAdmin, isSchemaAdmin, isSchemaEditingAdmin, isSerializedCollectionRef, isStorageReference, isTenantClaimSource, isTenantMembershipSource, isToMany, isUnsupported, isValidAppHost, isValidEngine, knownScopes, nestAdminCollectionKeys, nestAdminKeysOf, nestAdminPropertyKeys, nestCollectionPatchPaths, parseAppAddress, parseEnvBoolean, parseRelationAggregateSort, parseScope, parseStorageReference, policy, queue, registerDataSourceCapabilities, registerResourceKind, reservedPrefixFor, resetDeclaredQueueConsumers, resetDeclaredResources, resetDeclaredSubscriptions, resolveClientListLimit, resolveResourceRefs, resolveStorageReferences, resourceEnvSuffix, resourceId, resourceKeyOf, resourceKind, resourceKinds, resourceToDataSource, resourceToStorageSource, rewriteLegacyRlsFunctions, runtimeOnlyKeyIn, scopeAcceptsTarget, scopeGrants, scopeGrantsAny, scopeTargets, scopesBeyond, scopesForRoles, serializeCollections, setQueueRuntime, setTopicRuntime, sortKeyToString, storageEnvSuffix, storageObjectPath, storageReference, summarizeRoles, summarizeScopes, toCanonicalOp, toCollectionJson, topic, unsupportedMethod, usesLegacyRlsFunctions, validateScopes };
|
|
2478
3535
|
|
|
2479
3536
|
//# sourceMappingURL=index.es.js.map
|