@rebasepro/types 0.25.0 → 0.26.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.
@@ -4,6 +4,7 @@ import type { RebaseClient } from "./controllers/client.js";
4
4
  import type { RebaseSdkData } from "./controllers/data.js";
5
5
  import type { User } from "./users/index.js";
6
6
  import type { RegisteredDatabase } from "./types/register.js";
7
+ import type { CallerCredential } from "./types/api_keys.js";
7
8
  /**
8
9
  * The client a collection callback is handed as `context.client`: the Rebase
9
10
  * client **without `data`**.
@@ -159,4 +160,21 @@ export type RebaseCallContext<USER extends User = User, DB = RegisteredDatabase>
159
160
  * Set by the backend when callbacks are executed on the server.
160
161
  */
161
162
  user?: USER;
163
+ /**
164
+ * The API key the write was made with, when it was one: its `kind`, `id`
165
+ * and `name`. Absent for a person's own session, a service-key request,
166
+ * server-context work (`rebase.dataAsAdmin`, crons) and in the browser.
167
+ *
168
+ * A personal key acts as its owner, so {@link user} is the owner whether
169
+ * they saved by hand or a CI job wrote with their key. Read this to tell
170
+ * the two apart, for example to attribute an audit entry to the token.
171
+ *
172
+ * @example
173
+ * afterSave: async ({ context }) => {
174
+ * const who = context.credential
175
+ * ? `${context.credential.name} (key of ${context.user?.uid})`
176
+ * : context.user?.uid;
177
+ * }
178
+ */
179
+ credential?: CallerCredential;
162
180
  };
@@ -9,6 +9,7 @@ import type { ApiKeysAPI, PersonalKeysAPI } from "../types/api_keys.js";
9
9
  import type { RoleSummary } from "../types/scopes.js";
10
10
  import type { StorageSourceDefinition } from "../types/storage_source.js";
11
11
  import type { RegisteredDatabase } from "../types/register.js";
12
+ import type { AccessSimulation } from "../types/auth_adapter.js";
12
13
  /**
13
14
  * Event type for authentication state changes
14
15
  */
@@ -502,6 +503,24 @@ export interface RebaseClient<DB = unknown> {
502
503
  setOnUnauthorized?(handler: () => Promise<boolean>): void;
503
504
  /** Resolve the current auth token */
504
505
  resolveToken?(): Promise<string | null>;
506
+ /**
507
+ * A client whose data requests run with simulated access — as a role, a
508
+ * uid holding a role, or a service key — for an administrator checking
509
+ * what row-level security lets that identity read (the admin panel's
510
+ * "Simulate access" bar).
511
+ *
512
+ * Every request carries the `x-rebase-simulate-*` headers and this
513
+ * client's token; the server honours them on the data API and custom
514
+ * functions for an administrator's own session and refuses them anywhere
515
+ * else. The derived client opens no realtime socket (live queries fetch
516
+ * once), keeps no offline cache, and holds no session: a refusal of its
517
+ * token is answered by this client. Close it with `close()` when done.
518
+ *
519
+ * Read-only: a write, delete, batch, history revert or non-GET function
520
+ * call through it is refused with `403 SIMULATION_READ_ONLY`, since it
521
+ * would run as the simulated identity rather than the administrator.
522
+ */
523
+ simulate?(simulation: AccessSimulation): RebaseClient<Record<string, unknown>>;
505
524
  /**
506
525
  * POST to an arbitrary path on the backend — the escape hatch, not the way
507
526
  * to call a function.
@@ -522,10 +541,17 @@ export interface RebaseClient<DB = unknown> {
522
541
  /**
523
542
  * Execute raw SQL against the database.
524
543
  * Only available server-side with a SQL database.
544
+ *
545
+ * The same options as {@link RebaseServerClient.sql}: pass values through
546
+ * `params` as `$1`, `$2`, … rather than into the query text. This is what
547
+ * a collection callback's `context.client.sql` is typed by, and it used to
548
+ * have no `params`, so the one typed way to run SQL from a hook left
549
+ * interpolating row values into the string as the way to write it.
525
550
  */
526
551
  sql?(query: string, options?: {
527
- database?: string;
552
+ database?: ResourceRef;
528
553
  role?: string;
554
+ params?: unknown[];
529
555
  }): Promise<Record<string, unknown>[]>;
530
556
  }
531
557
  /**
@@ -568,6 +594,11 @@ export interface RebaseServerClient<DB = RegisteredDatabase> extends Omit<Rebase
568
594
  * engines. Values interpolated into the query should be passed via
569
595
  * `params`, referenced as `$1`, `$2`, … placeholders in the query text.
570
596
  *
597
+ * `database` names a declared data source — the handle `database("analytics")`
598
+ * returned, or its key — and the query runs on that source's own
599
+ * connection. Omitted, it runs on the default source. A key no configured
600
+ * source has is refused by name.
601
+ *
571
602
  * **Runtime note.** This is the one accessor on this object that is not
572
603
  * portable. It runs on the database owner connection over a TCP socket, so
573
604
  * it is available wherever the framework holds that connection — every Node
@@ -593,6 +593,28 @@ export interface DataDriver {
593
593
  * Optional: a driver without it is called directly, read by read.
594
594
  */
595
595
  readTogether?<T>(work: (driver: DataDriver) => Promise<T>): Promise<T>;
596
+ /**
597
+ * Every row a collection stores, as stored, a page at a time: soft-deleted
598
+ * rows included, and past every read hook — no `beforeQuery`, no
599
+ * `afterRead`, no per-field read rules.
600
+ *
601
+ * For whole-store sweeps run by the server itself, never for a read served
602
+ * to anyone. `rebase storage orphans` reads it to find the files no row
603
+ * names: read through `fetchCollection` instead, a `beforeQuery` scoping
604
+ * rows to `context.user` (there is none in a sweep) or an `afterRead` that
605
+ * rewrites a file field hides references, and the files they name are
606
+ * reported as unused and deleted.
607
+ *
608
+ * Optional. A sweep reads a collection without read hooks through
609
+ * `fetchCollection`, and refuses one with read hooks whose driver lacks
610
+ * this, rather than trust what the hooks let through.
611
+ */
612
+ scanStoredRows?(props: {
613
+ path: string;
614
+ collection: CollectionConfig;
615
+ /** Rows per page. */
616
+ pageSize?: number;
617
+ }): AsyncIterable<Record<string, unknown>[]>;
596
618
  /**
597
619
  * Check if the given filter combination is valid
598
620
  * @param props
package/dist/index.es.js CHANGED
@@ -2348,6 +2348,108 @@ function isLazyComponentRef(ref) {
2348
2348
  * @group Auth
2349
2349
  */
2350
2350
  var IMPERSONATE_HEADER = "x-rebase-impersonate";
2351
+ /**
2352
+ * The request header that asks for a data-plane request to run as a role —
2353
+ * the RLS simulator's "view as". Alone it runs as a signed-in holder of that
2354
+ * role who owns no rows; with {@link SIMULATE_USER_ID_HEADER} it runs as that
2355
+ * uid holding that role, whether or not such an account exists.
2356
+ *
2357
+ * `anon` (or `anonymous`) runs as a caller with no session at all.
2358
+ *
2359
+ * Like {@link IMPERSONATE_HEADER}, only a signed-in administrator's own session
2360
+ * may send it; anyone else is refused with 403, never answered as themselves.
2361
+ *
2362
+ * @group Auth
2363
+ */
2364
+ var SIMULATE_ROLE_HEADER = "x-rebase-simulate-role";
2365
+ /**
2366
+ * The uid a simulated request runs as. With {@link SIMULATE_ROLE_HEADER} it is
2367
+ * a hypothetical — "u_1 holding customer" — and need not name an account;
2368
+ * without it, the uid must be an active user and runs with their roles now.
2369
+ *
2370
+ * @group Auth
2371
+ */
2372
+ var SIMULATE_USER_ID_HEADER = "x-rebase-simulate-user-id";
2373
+ /**
2374
+ * The id of a service API key a simulated request runs as: the key's identity,
2375
+ * its RLS roles and its scopes, exactly as a request presenting the key would.
2376
+ * Excludes the other two simulation headers.
2377
+ *
2378
+ * @group Auth
2379
+ */
2380
+ var SIMULATE_API_KEY_ID_HEADER = "x-rebase-simulate-api-key-id";
2381
+ /**
2382
+ * The response header naming what a simulated request ran as:
2383
+ * `role=<role>;sub=<uid>;masked_columns=<a,b>`, plus `api_key=<id>` and
2384
+ * `collection=<slug>` when they apply. Every value is URI-encoded. Read it
2385
+ * with {@link parseSimulatedContext}.
2386
+ *
2387
+ * @group Auth
2388
+ */
2389
+ var SIMULATED_CONTEXT_HEADER = "x-rebase-simulated-context";
2390
+ /**
2391
+ * The request headers for an {@link AccessSimulation}. Empty when it asks for
2392
+ * nothing, so spreading it into a header set is always safe.
2393
+ *
2394
+ * @group Auth
2395
+ */
2396
+ function simulationHeaders(simulation) {
2397
+ if (!simulation) return {};
2398
+ const headers = {};
2399
+ if (simulation.role) headers[SIMULATE_ROLE_HEADER] = simulation.role;
2400
+ if (simulation.userId) headers[SIMULATE_USER_ID_HEADER] = simulation.userId;
2401
+ if (simulation.apiKeyId) headers[SIMULATE_API_KEY_ID_HEADER] = simulation.apiKeyId;
2402
+ return headers;
2403
+ }
2404
+ /**
2405
+ * Write a {@link SimulatedContext} as the header value.
2406
+ *
2407
+ * @group Auth
2408
+ */
2409
+ function formatSimulatedContext(context) {
2410
+ const parts = [
2411
+ `role=${encodeURIComponent(context.role)}`,
2412
+ `sub=${encodeURIComponent(context.sub)}`,
2413
+ `masked_columns=${context.maskedColumns.map(encodeURIComponent).join(",")}`
2414
+ ];
2415
+ if (context.apiKeyId) parts.push(`api_key=${encodeURIComponent(context.apiKeyId)}`);
2416
+ if (context.collection) parts.push(`collection=${encodeURIComponent(context.collection)}`);
2417
+ return parts.join(";");
2418
+ }
2419
+ /**
2420
+ * Read {@link SIMULATED_CONTEXT_HEADER}. `undefined` for an absent header or one
2421
+ * missing `role` or `sub`.
2422
+ *
2423
+ * @group Auth
2424
+ */
2425
+ function parseSimulatedContext(value) {
2426
+ if (!value) return void 0;
2427
+ const fields = /* @__PURE__ */ new Map();
2428
+ for (const part of value.split(";")) {
2429
+ const at = part.indexOf("=");
2430
+ if (at > 0) fields.set(part.slice(0, at).trim(), part.slice(at + 1).trim());
2431
+ }
2432
+ const role = fields.get("role");
2433
+ const sub = fields.get("sub");
2434
+ if (role === void 0 || sub === void 0) return void 0;
2435
+ const decode = (raw) => {
2436
+ try {
2437
+ return decodeURIComponent(raw);
2438
+ } catch {
2439
+ return raw;
2440
+ }
2441
+ };
2442
+ const masked = fields.get("masked_columns") ?? "";
2443
+ const apiKeyId = fields.get("api_key");
2444
+ const collection = fields.get("collection");
2445
+ return {
2446
+ role: decode(role),
2447
+ sub: decode(sub),
2448
+ maskedColumns: masked === "" ? [] : masked.split(",").map(decode),
2449
+ ...apiKeyId ? { apiKeyId: decode(apiKeyId) } : {},
2450
+ ...collection ? { collection: decode(collection) } : {}
2451
+ };
2452
+ }
2351
2453
  //#endregion
2352
2454
  //#region src/types/scopes.ts
2353
2455
  /**
@@ -2563,6 +2665,25 @@ function hasAdminRole(roles) {
2563
2665
  return !!roles?.includes(ADMIN_ROLE);
2564
2666
  }
2565
2667
  /**
2668
+ * Why `name` cannot be written as a role, or `undefined` when it can.
2669
+ *
2670
+ * No naming pattern is imposed — an app may call its roles anything. What is
2671
+ * refused is a name that would be read back as *other* roles: a role travels as
2672
+ * one element of a Postgres `text[]` and as one entry of the comma-joined
2673
+ * `app.user_roles` setting that RLS policies split with
2674
+ * `string_to_array(…, ',')`. Written as `"support,admin"`, a role comes back as
2675
+ * `admin`; braces, quotes and backslashes are array-literal syntax, so they are
2676
+ * refused too, as are empty names and control characters.
2677
+ *
2678
+ * @group Auth
2679
+ */
2680
+ function roleNameProblem(name) {
2681
+ if (typeof name !== "string") return "is not a string";
2682
+ if (name.trim() === "") return "is empty";
2683
+ if (/[,{}"\\]/.test(name)) return "contains , { } \" or \\";
2684
+ if (/[\u0000-\u001f\u007f]/.test(name)) return "contains a control character";
2685
+ }
2686
+ /**
2566
2687
  * Does a set of held scopes grant `scope`, optionally on one `target`?
2567
2688
  *
2568
2689
  * The unqualified grant covers every target. A qualified grant covers its own
@@ -3694,6 +3815,6 @@ function resolverFromSources(client) {
3694
3815
  };
3695
3816
  }
3696
3817
  //#endregion
3697
- 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, INCLUDE_COUNT_KEY, 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, isContentHashedFileName, 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, storageReferencesForDownloadUrls, storageTargetOfDownloadUrl, storageTargetsInText, summarizeRoles, summarizeScopes, toCanonicalOp, toCollectionJson, topic, unsupportedMethod, usesLegacyRlsFunctions, validateScopes };
3818
+ 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, INCLUDE_COUNT_KEY, 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, SIMULATED_CONTEXT_HEADER, SIMULATE_API_KEY_ID_HEADER, SIMULATE_ROLE_HEADER, SIMULATE_USER_ID_HEADER, 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, formatSimulatedContext, getCollectionDataPath, getDataSourceCapabilities, getDeclaredSubcollections, hasAdminRole, hasFieldOperation, hasForeignKeyOnTarget, intersectScopes, isAdminScope, isAnonymousUid, isBranchAdmin, isBuiltInScope, isChannelBusInstance, isCollectionPatch, isContentHashedFileName, 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, parseSimulatedContext, parseStorageReference, policy, queue, registerDataSourceCapabilities, registerResourceKind, reservedPrefixFor, resetDeclaredQueueConsumers, resetDeclaredResources, resetDeclaredSubscriptions, resolveClientListLimit, resolveResourceRefs, resolveStorageReferences, resourceEnvSuffix, resourceId, resourceKeyOf, resourceKind, resourceKinds, resourceToDataSource, resourceToStorageSource, rewriteLegacyRlsFunctions, roleNameProblem, runtimeOnlyKeyIn, scopeAcceptsTarget, scopeGrants, scopeGrantsAny, scopeTargets, scopesBeyond, scopesForRoles, serializeCollections, setQueueRuntime, setTopicRuntime, simulationHeaders, sortKeyToString, storageEnvSuffix, storageObjectPath, storageReference, storageReferencesForDownloadUrls, storageTargetOfDownloadUrl, storageTargetsInText, summarizeRoles, summarizeScopes, toCanonicalOp, toCollectionJson, topic, unsupportedMethod, usesLegacyRlsFunctions, validateScopes };
3698
3819
 
3699
3820
  //# sourceMappingURL=index.es.js.map