graphile-presigned-url-plugin 1.11.3 → 1.12.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.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Server-side bucket resolution.
3
+ *
4
+ * Which bucket a write lands in belongs to the database, never to the client and
5
+ * never to the server's environment: a client-chosen key means a different
6
+ * bucket per tenant, and an env-level bucket name means storage that belongs to
7
+ * no tenant at all. `function_resolution.resolve_default_bucket` is the one
8
+ * place that answers it — a logical key when the field declares one, otherwise
9
+ * the reserved default tag for the requested access ('default' / 'default-public').
10
+ *
11
+ * Zero matches and several matches both raise inside SQL, so there is nothing to
12
+ * guess here: this module only carries the question in and the coordinate out.
13
+ */
14
+ /**
15
+ * The resolved bucket coordinate.
16
+ *
17
+ * `physicalName` is the recorded S3 bucket name, or null when the logical
18
+ * bucket has never been provisioned — the caller mints and records it then.
19
+ */
20
+ export interface ResolvedBucketCoordinate {
21
+ bucketId: string;
22
+ resolvedKey: string;
23
+ bucketType: 'public' | 'private' | 'temp';
24
+ physicalName: string | null;
25
+ }
26
+ /**
27
+ * Resolve the bucket a write should land in.
28
+ *
29
+ * @param scope - The storage module's scope ('app' for database-wide storage)
30
+ * @param entityId - The owning entity row for an entity-scoped module, else null
31
+ * @param publicAccess - Which reserved default tag to use when no key is named,
32
+ * and an assertion on the named bucket's type when one is
33
+ * @param bucketKey - The field's declared logical key, or null for the default
34
+ */
35
+ export declare function resolveDefaultBucket(pgClient: {
36
+ query: (opts: {
37
+ text: string;
38
+ values?: unknown[];
39
+ }) => Promise<{
40
+ rows: unknown[];
41
+ }>;
42
+ }, databaseId: string, scope: string, entityId: string | null, publicAccess: boolean, bucketKey: string | null): Promise<ResolvedBucketCoordinate>;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * Server-side bucket resolution.
4
+ *
5
+ * Which bucket a write lands in belongs to the database, never to the client and
6
+ * never to the server's environment: a client-chosen key means a different
7
+ * bucket per tenant, and an env-level bucket name means storage that belongs to
8
+ * no tenant at all. `function_resolution.resolve_default_bucket` is the one
9
+ * place that answers it — a logical key when the field declares one, otherwise
10
+ * the reserved default tag for the requested access ('default' / 'default-public').
11
+ *
12
+ * Zero matches and several matches both raise inside SQL, so there is nothing to
13
+ * guess here: this module only carries the question in and the coordinate out.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.resolveDefaultBucket = resolveDefaultBucket;
17
+ const logger_1 = require("@pgpmjs/logger");
18
+ const log = new logger_1.Logger('graphile-presigned-url:default-bucket');
19
+ const RESOLVE_DEFAULT_BUCKET_QUERY = `
20
+ SELECT bucket_id, resolved_key, bucket_type, physical_name
21
+ FROM function_resolution.resolve_default_bucket($1, $2, $3, $4, $5)
22
+ `;
23
+ /**
24
+ * Resolve the bucket a write should land in.
25
+ *
26
+ * @param scope - The storage module's scope ('app' for database-wide storage)
27
+ * @param entityId - The owning entity row for an entity-scoped module, else null
28
+ * @param publicAccess - Which reserved default tag to use when no key is named,
29
+ * and an assertion on the named bucket's type when one is
30
+ * @param bucketKey - The field's declared logical key, or null for the default
31
+ */
32
+ async function resolveDefaultBucket(pgClient, databaseId, scope, entityId, publicAccess, bucketKey) {
33
+ const result = await pgClient.query({
34
+ text: RESOLVE_DEFAULT_BUCKET_QUERY,
35
+ values: [databaseId, scope, entityId, publicAccess, bucketKey],
36
+ });
37
+ const row = result.rows[0];
38
+ if (!row) {
39
+ // resolve_default_bucket raises on zero and on several matches, so an empty
40
+ // result means the function did not run as declared rather than "no bucket".
41
+ throw new Error(`STORAGE_DEFAULT_BUCKET_NO_ROW: resolve_default_bucket returned no row for ` +
42
+ `database=${databaseId} scope=${scope} public=${publicAccess} key=${bucketKey ?? '<default tag>'}`);
43
+ }
44
+ log.debug(`Resolved bucket ${row.resolved_key} (${row.bucket_type}) for database=${databaseId} scope=${scope}`);
45
+ return {
46
+ bucketId: row.bucket_id,
47
+ resolvedKey: row.resolved_key,
48
+ bucketType: row.bucket_type,
49
+ physicalName: row.physical_name,
50
+ };
51
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Server-side bucket resolution.
3
+ *
4
+ * Which bucket a write lands in belongs to the database, never to the client and
5
+ * never to the server's environment: a client-chosen key means a different
6
+ * bucket per tenant, and an env-level bucket name means storage that belongs to
7
+ * no tenant at all. `function_resolution.resolve_default_bucket` is the one
8
+ * place that answers it — a logical key when the field declares one, otherwise
9
+ * the reserved default tag for the requested access ('default' / 'default-public').
10
+ *
11
+ * Zero matches and several matches both raise inside SQL, so there is nothing to
12
+ * guess here: this module only carries the question in and the coordinate out.
13
+ */
14
+ /**
15
+ * The resolved bucket coordinate.
16
+ *
17
+ * `physicalName` is the recorded S3 bucket name, or null when the logical
18
+ * bucket has never been provisioned — the caller mints and records it then.
19
+ */
20
+ export interface ResolvedBucketCoordinate {
21
+ bucketId: string;
22
+ resolvedKey: string;
23
+ bucketType: 'public' | 'private' | 'temp';
24
+ physicalName: string | null;
25
+ }
26
+ /**
27
+ * Resolve the bucket a write should land in.
28
+ *
29
+ * @param scope - The storage module's scope ('app' for database-wide storage)
30
+ * @param entityId - The owning entity row for an entity-scoped module, else null
31
+ * @param publicAccess - Which reserved default tag to use when no key is named,
32
+ * and an assertion on the named bucket's type when one is
33
+ * @param bucketKey - The field's declared logical key, or null for the default
34
+ */
35
+ export declare function resolveDefaultBucket(pgClient: {
36
+ query: (opts: {
37
+ text: string;
38
+ values?: unknown[];
39
+ }) => Promise<{
40
+ rows: unknown[];
41
+ }>;
42
+ }, databaseId: string, scope: string, entityId: string | null, publicAccess: boolean, bucketKey: string | null): Promise<ResolvedBucketCoordinate>;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Server-side bucket resolution.
3
+ *
4
+ * Which bucket a write lands in belongs to the database, never to the client and
5
+ * never to the server's environment: a client-chosen key means a different
6
+ * bucket per tenant, and an env-level bucket name means storage that belongs to
7
+ * no tenant at all. `function_resolution.resolve_default_bucket` is the one
8
+ * place that answers it — a logical key when the field declares one, otherwise
9
+ * the reserved default tag for the requested access ('default' / 'default-public').
10
+ *
11
+ * Zero matches and several matches both raise inside SQL, so there is nothing to
12
+ * guess here: this module only carries the question in and the coordinate out.
13
+ */
14
+ import { Logger } from '@pgpmjs/logger';
15
+ const log = new Logger('graphile-presigned-url:default-bucket');
16
+ const RESOLVE_DEFAULT_BUCKET_QUERY = `
17
+ SELECT bucket_id, resolved_key, bucket_type, physical_name
18
+ FROM function_resolution.resolve_default_bucket($1, $2, $3, $4, $5)
19
+ `;
20
+ /**
21
+ * Resolve the bucket a write should land in.
22
+ *
23
+ * @param scope - The storage module's scope ('app' for database-wide storage)
24
+ * @param entityId - The owning entity row for an entity-scoped module, else null
25
+ * @param publicAccess - Which reserved default tag to use when no key is named,
26
+ * and an assertion on the named bucket's type when one is
27
+ * @param bucketKey - The field's declared logical key, or null for the default
28
+ */
29
+ export async function resolveDefaultBucket(pgClient, databaseId, scope, entityId, publicAccess, bucketKey) {
30
+ const result = await pgClient.query({
31
+ text: RESOLVE_DEFAULT_BUCKET_QUERY,
32
+ values: [databaseId, scope, entityId, publicAccess, bucketKey],
33
+ });
34
+ const row = result.rows[0];
35
+ if (!row) {
36
+ // resolve_default_bucket raises on zero and on several matches, so an empty
37
+ // result means the function did not run as declared rather than "no bucket".
38
+ throw new Error(`STORAGE_DEFAULT_BUCKET_NO_ROW: resolve_default_bucket returned no row for ` +
39
+ `database=${databaseId} scope=${scope} public=${publicAccess} key=${bucketKey ?? '<default tag>'}`);
40
+ }
41
+ log.debug(`Resolved bucket ${row.resolved_key} (${row.bucket_type}) for database=${databaseId} scope=${scope}`);
42
+ return {
43
+ bucketId: row.bucket_id,
44
+ resolvedKey: row.resolved_key,
45
+ bucketType: row.bucket_type,
46
+ physicalName: row.physical_name,
47
+ };
48
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The `file_ref_field` registry: which storage module and bucket a managed
3
+ * document column writes into.
4
+ *
5
+ * An `image`/`upload` column is a projection of a files row, and the decision of
6
+ * *where* those bytes live is a property of the field declaration, not of the
7
+ * request. The registry records that intent per (table, column) — a storage
8
+ * module plus either a logical bucket key, a tag selector, or nothing at all
9
+ * (meaning the reserved default tag for the declared publicness).
10
+ *
11
+ * This module answers one question — "what does a write to this column bind
12
+ * to?" — and answers it loudly: an unregistered column raises rather than
13
+ * falling back to a server-global bucket, because a silent fallback is how the
14
+ * unmanaged lane produced objects no tenant owned.
15
+ */
16
+ /**
17
+ * A field's recorded storage intent.
18
+ *
19
+ * `bucketKey` and `bucketTags` are mutually exclusive by table constraint, and
20
+ * both may be absent — resolution then uses the reserved default tag for
21
+ * `isPublic`. Nothing here is a physical bucket name or id: the concrete bucket
22
+ * is resolved per written row, inside the tenant.
23
+ */
24
+ export interface FileRefFieldBinding {
25
+ id: string;
26
+ storageModuleId: string;
27
+ bucketKey: string | null;
28
+ bucketTags: string[] | null;
29
+ isPublic: boolean | null;
30
+ enforceFk: boolean;
31
+ }
32
+ export declare class FileRefFieldNotRegisteredError extends Error {
33
+ readonly databaseId: string;
34
+ readonly schemaName: string;
35
+ readonly tableName: string;
36
+ readonly columnName: string;
37
+ constructor(databaseId: string, schemaName: string, tableName: string, columnName: string);
38
+ }
39
+ /**
40
+ * Look up the storage binding for a document column, or throw.
41
+ *
42
+ * The read runs on whichever client the caller passes. The registry is schema
43
+ * metadata rather than tenant rows, so callers resolve it in the system lane —
44
+ * the RLS that matters is on the files table the upload eventually writes.
45
+ */
46
+ export declare function getFileRefFieldBinding(pgClient: {
47
+ query: (opts: {
48
+ text: string;
49
+ values?: unknown[];
50
+ }) => Promise<{
51
+ rows: unknown[];
52
+ }>;
53
+ }, databaseId: string, field: {
54
+ schemaName: string;
55
+ tableName: string;
56
+ columnName: string;
57
+ }): Promise<FileRefFieldBinding>;
58
+ /**
59
+ * Drop cached bindings. Used by tests and after a re-provision.
60
+ */
61
+ export declare function clearFileRefFieldCache(): void;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The `file_ref_field` registry: which storage module and bucket a managed
3
+ * document column writes into.
4
+ *
5
+ * An `image`/`upload` column is a projection of a files row, and the decision of
6
+ * *where* those bytes live is a property of the field declaration, not of the
7
+ * request. The registry records that intent per (table, column) — a storage
8
+ * module plus either a logical bucket key, a tag selector, or nothing at all
9
+ * (meaning the reserved default tag for the declared publicness).
10
+ *
11
+ * This module answers one question — "what does a write to this column bind
12
+ * to?" — and answers it loudly: an unregistered column raises rather than
13
+ * falling back to a server-global bucket, because a silent fallback is how the
14
+ * unmanaged lane produced objects no tenant owned.
15
+ */
16
+ import { Logger } from '@pgpmjs/logger';
17
+ import { LRUCache } from 'lru-cache';
18
+ const log = new Logger('graphile-presigned-url:file-ref-registry');
19
+ const FIVE_MINUTES_MS = 1000 * 60 * 5;
20
+ const ONE_HOUR_MS = 1000 * 60 * 60;
21
+ /**
22
+ * Resolve the registry row for a document column.
23
+ *
24
+ * Joined through metaschema rather than keyed by name, because the registry
25
+ * records field *ids*: the physical (schema, table, column) triple is what the
26
+ * GraphQL layer knows, and metaschema is the only thing that maps one to the
27
+ * other.
28
+ */
29
+ const FILE_REF_FIELD_QUERY = `
30
+ SELECT
31
+ frf.id,
32
+ frf.storage_module_id,
33
+ frf.bucket_key,
34
+ frf.bucket_tags::text[] AS bucket_tags,
35
+ frf.is_public,
36
+ frf.enforce_fk
37
+ FROM metaschema_modules_public.file_ref_field frf
38
+ JOIN metaschema_public.field f ON f.id = frf.field_id
39
+ JOIN metaschema_public.table t ON t.id = frf.table_id
40
+ JOIN metaschema_public.schema s ON s.id = t.schema_id
41
+ WHERE frf.database_id = $1
42
+ AND s.schema_name = $2
43
+ AND t.name = $3
44
+ AND f.name = $4
45
+ LIMIT 1
46
+ `;
47
+ /**
48
+ * LRU cache of field bindings.
49
+ *
50
+ * A binding is schema, not data: it changes only when a database is
51
+ * re-provisioned, so it caches on the same terms as the storage module config
52
+ * next to it. Misses are never cached — an unregistered column is a hard error
53
+ * every time it is written, not a remembered "no".
54
+ */
55
+ const bindingCache = new LRUCache({
56
+ max: 500,
57
+ ttl: process.env.NODE_ENV === 'development' ? FIVE_MINUTES_MS : ONE_HOUR_MS,
58
+ updateAgeOnGet: true,
59
+ });
60
+ export class FileRefFieldNotRegisteredError extends Error {
61
+ databaseId;
62
+ schemaName;
63
+ tableName;
64
+ columnName;
65
+ constructor(databaseId, schemaName, tableName, columnName) {
66
+ super(`FILE_REF_FIELD_NOT_REGISTERED: ${schemaName}.${tableName}.${columnName} ` +
67
+ `is not a registered file-reference field in database ${databaseId}. ` +
68
+ 'A managed upload needs the declared storage module and bucket intent; ' +
69
+ 'there is no server-global bucket to fall back to.');
70
+ this.databaseId = databaseId;
71
+ this.schemaName = schemaName;
72
+ this.tableName = tableName;
73
+ this.columnName = columnName;
74
+ this.name = 'FileRefFieldNotRegisteredError';
75
+ }
76
+ }
77
+ /**
78
+ * Look up the storage binding for a document column, or throw.
79
+ *
80
+ * The read runs on whichever client the caller passes. The registry is schema
81
+ * metadata rather than tenant rows, so callers resolve it in the system lane —
82
+ * the RLS that matters is on the files table the upload eventually writes.
83
+ */
84
+ export async function getFileRefFieldBinding(pgClient, databaseId, field) {
85
+ const cacheKey = `file-ref:${databaseId}:${field.schemaName}.${field.tableName}.${field.columnName}`;
86
+ const cached = bindingCache.get(cacheKey);
87
+ if (cached)
88
+ return cached;
89
+ const result = await pgClient.query({
90
+ text: FILE_REF_FIELD_QUERY,
91
+ values: [databaseId, field.schemaName, field.tableName, field.columnName],
92
+ });
93
+ if (result.rows.length === 0) {
94
+ throw new FileRefFieldNotRegisteredError(databaseId, field.schemaName, field.tableName, field.columnName);
95
+ }
96
+ const row = result.rows[0];
97
+ const binding = {
98
+ id: row.id,
99
+ storageModuleId: row.storage_module_id,
100
+ bucketKey: row.bucket_key,
101
+ bucketTags: row.bucket_tags,
102
+ isPublic: row.is_public,
103
+ enforceFk: row.enforce_fk,
104
+ };
105
+ bindingCache.set(cacheKey, binding);
106
+ log.debug(`Bound ${field.schemaName}.${field.tableName}.${field.columnName} to storage module ` +
107
+ `${binding.storageModuleId} (bucket_key=${binding.bucketKey ?? '<default tag>'})`);
108
+ return binding;
109
+ }
110
+ /**
111
+ * Drop cached bindings. Used by tests and after a re-provision.
112
+ */
113
+ export function clearFileRefFieldCache() {
114
+ bindingCache.clear();
115
+ }
package/esm/index.d.ts CHANGED
@@ -26,9 +26,16 @@
26
26
  * };
27
27
  * ```
28
28
  */
29
+ export type { ResolvedBucketCoordinate } from './default-bucket';
30
+ export { resolveDefaultBucket } from './default-bucket';
29
31
  export { createDownloadUrlPlugin } from './download-url-field';
32
+ export type { FileRefFieldBinding } from './file-ref-registry';
33
+ export { clearFileRefFieldCache, FileRefFieldNotRegisteredError, getFileRefFieldBinding } from './file-ref-registry';
34
+ export { assertUploadAllowedByBucket, buildFileProjection, type FileProjection, finalizeStagedUpload, type ManagedUploadTarget, resolveManagedUploadTarget, } from './managed-upload';
35
+ export { mintPhysicalBucketName, provisionAndRecordPhysicalBucket, resolveS3, resolveS3ForDatabase } from './physical-bucket';
30
36
  export { createPresignedUrlPlugin, PresignedUrlPlugin } from './plugin';
31
37
  export { PresignedUrlPreset } from './preset';
32
- export { deleteS3Object, generatePresignedGetUrl, generatePresignedPutUrl, headObject } from './s3-signer';
38
+ export { type WithPgClient, withRequestPgClient } from './request-pg-client';
39
+ export { copyS3Object, deleteS3Object, generatePresignedGetUrl, generatePresignedPutUrl, headObject } from './s3-signer';
33
40
  export { clearBucketCache, clearStorageModuleCache, getBucketConfig, getStorageModuleConfig, getStorageModuleConfigForOwner, isS3BucketProvisioned, loadAllStorageModules, markS3BucketProvisioned, resolveStorageConfigFromCodec, resolveStorageModuleByFileId } from './storage-module-cache';
34
41
  export type { BucketConfig, BucketNameResolver, EnsureBucketProvisioned, PresignedUrlPluginOptions, RequestUploadUrlInput, RequestUploadUrlPayload, S3Config, S3ConfigOrGetter, StorageModuleConfig, } from './types';
package/esm/index.js CHANGED
@@ -26,8 +26,13 @@
26
26
  * };
27
27
  * ```
28
28
  */
29
+ export { resolveDefaultBucket } from './default-bucket';
29
30
  export { createDownloadUrlPlugin } from './download-url-field';
31
+ export { clearFileRefFieldCache, FileRefFieldNotRegisteredError, getFileRefFieldBinding } from './file-ref-registry';
32
+ export { assertUploadAllowedByBucket, buildFileProjection, finalizeStagedUpload, resolveManagedUploadTarget, } from './managed-upload';
33
+ export { mintPhysicalBucketName, provisionAndRecordPhysicalBucket, resolveS3, resolveS3ForDatabase } from './physical-bucket';
30
34
  export { createPresignedUrlPlugin, PresignedUrlPlugin } from './plugin';
31
35
  export { PresignedUrlPreset } from './preset';
32
- export { deleteS3Object, generatePresignedGetUrl, generatePresignedPutUrl, headObject } from './s3-signer';
36
+ export { withRequestPgClient } from './request-pg-client';
37
+ export { copyS3Object, deleteS3Object, generatePresignedGetUrl, generatePresignedPutUrl, headObject } from './s3-signer';
33
38
  export { clearBucketCache, clearStorageModuleCache, getBucketConfig, getStorageModuleConfig, getStorageModuleConfigForOwner, isS3BucketProvisioned, loadAllStorageModules, markS3BucketProvisioned, resolveStorageConfigFromCodec, resolveStorageModuleByFileId } from './storage-module-cache';
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The managed upload lifecycle, shared by both transports.
3
+ *
4
+ * Multipart-through-GraphQL and presigned two-step are transports for one
5
+ * lifecycle, not two data models: either way an object gets a files row, a
6
+ * server-chosen key, a tenant-resolved bucket, and a projection document that
7
+ * carries the files row's id. The only difference is who moves the bytes.
8
+ *
9
+ * This module owns the parts that are the same:
10
+ * * `resolveManagedUploadTarget` — from a document column to a concrete
11
+ * (storage module, bucket, physical bucket, S3 config).
12
+ * * `finalizeStagedUpload` — from bytes already in S3 under a staging key to a
13
+ * files row and a projection document, deduplicating on content hash.
14
+ * * `buildFileProjection` — the document shape the column stores.
15
+ *
16
+ * Nothing here bakes a presigned URL into a row: `url` is populated only for a
17
+ * public bucket, where it is a stable CDN address rather than a credential with
18
+ * an expiry.
19
+ */
20
+ import { type FileRefFieldBinding } from './file-ref-registry';
21
+ import { type WithPgClient } from './request-pg-client';
22
+ import type { BucketConfig, FileProjection, PresignedUrlPluginOptions, S3Config, StorageModuleConfig } from './types';
23
+ /**
24
+ * The document a managed `image`/`upload` column stores.
25
+ *
26
+ * `id` is the files row — the load-bearing field: it is what makes the column a
27
+ * projection rather than a second, unmanaged copy of the truth, and it is what
28
+ * storage GC counts before collecting an object.
29
+ *
30
+ * `url` is retained for existing readers of the pre-managed shape and is set
31
+ * only for public buckets. Prefer `id` plus the files row's `downloadUrl`, which
32
+ * is late-bound and works for private buckets too.
33
+ */
34
+ export type { FileProjection } from './types';
35
+ /**
36
+ * Build the projection document for a files row.
37
+ *
38
+ * A public bucket has a stable address, so `url` is a real, durable value there.
39
+ * A private bucket has no such address — only presigned, expiring ones — so the
40
+ * field is omitted rather than filled with a URL that dies in an hour.
41
+ */
42
+ export declare function buildFileProjection(file: {
43
+ id: string;
44
+ key: string;
45
+ bucketId: string;
46
+ mime: string;
47
+ size: number;
48
+ filename?: string | null;
49
+ }, bucket: {
50
+ is_public: boolean;
51
+ }, s3: S3Config): FileProjection;
52
+ /**
53
+ * Everything a managed upload needs before bytes move.
54
+ */
55
+ export interface ManagedUploadTarget {
56
+ databaseId: string;
57
+ storageConfig: StorageModuleConfig;
58
+ bucket: BucketConfig;
59
+ physicalName: string;
60
+ s3: S3Config;
61
+ /** The registry row, or null when the column predates registration. */
62
+ binding: FileRefFieldBinding | null;
63
+ }
64
+ /**
65
+ * Resolve where a write to a document column lands.
66
+ *
67
+ * Two routes, one rule — the bucket is always resolved inside the tenant:
68
+ * * a registered column names its storage module, and either a logical bucket
69
+ * key or the reserved default tag for its declared publicness;
70
+ * * an unregistered column (a bare `image`/`upload` on a database provisioned
71
+ * before the registry) falls back to the app-scope module and the same
72
+ * reserved default tag. That is a *tenant* default, not an environment one.
73
+ *
74
+ * A database with no storage module raises: there is nowhere tenant-owned to put
75
+ * the bytes, and the deployment's configured bucket is not an answer.
76
+ */
77
+ export declare function resolveManagedUploadTarget(args: {
78
+ options: PresignedUrlPluginOptions;
79
+ withPgClient: WithPgClient;
80
+ pgSettings: Record<string, string> | null;
81
+ databaseId: string;
82
+ field: {
83
+ schemaName: string;
84
+ tableName: string;
85
+ columnName: string;
86
+ };
87
+ /** Publicness to use when the column is unregistered. */
88
+ defaultPublicAccess: boolean;
89
+ }): Promise<ManagedUploadTarget>;
90
+ /**
91
+ * Validate an upload against the resolved bucket's rules.
92
+ *
93
+ * The same rules the presigned lane enforces — a transport must not be a way
94
+ * around a bucket's mime allowlist or size cap.
95
+ */
96
+ export declare function assertUploadAllowedByBucket(target: ManagedUploadTarget, contentType: string, size: number): void;
97
+ /**
98
+ * Turn bytes already staged in S3 into a files row and a projection document.
99
+ *
100
+ * The content hash is only known once the stream has been read, so a streaming
101
+ * transport writes to a staging key first and promotes here:
102
+ *
103
+ * * hash already present in this bucket → drop the staged object, reuse the
104
+ * existing files row. Dedup is a property of the object, so it holds no
105
+ * matter which transport wrote it first.
106
+ * * otherwise → server-side copy to the content-addressed key, drop the staged
107
+ * object, insert the files row.
108
+ *
109
+ * The row is inserted *after* the bytes land, so the confirm-upload job the
110
+ * insert trigger enqueues finds the object and completes the
111
+ * `requested → uploaded` transition without any extra wiring here.
112
+ *
113
+ * Every failure path leaves S3 as it found it. Bytes written by this call and
114
+ * not reachable through a files row would be invisible to storage GC, which
115
+ * collects objects by walking rows — so an object is only left behind once the
116
+ * row naming it exists.
117
+ */
118
+ export declare function finalizeStagedUpload(args: {
119
+ target: ManagedUploadTarget;
120
+ withPgClient: WithPgClient;
121
+ pgSettings: Record<string, string> | null;
122
+ staged: {
123
+ stagingKey: string;
124
+ contentHash: string;
125
+ contentType: string;
126
+ size: number;
127
+ filename?: string | null;
128
+ };
129
+ }): Promise<{
130
+ projection: FileProjection;
131
+ deduplicated: boolean;
132
+ }>;