graphile-settings 6.18.0 → 6.18.1

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.
@@ -30,7 +30,7 @@ export declare function getPresignedUrlS3Config(): S3Config;
30
30
  * URL plugin (argument order: `(databaseId, bucketKey)`).
31
31
  *
32
32
  * Uses CDN_BUCKET_NAME as a prefix. For each (database, bucketKey) pair, the
33
- * S3 bucket name becomes `{prefix}-{bucketKey}-{databaseId}`.
33
+ * S3 bucket name becomes `{prefix}-{bucketKey}-{digest}`.
34
34
  *
35
35
  * This aligns with the bucket provisioner plugin which creates separate
36
36
  * S3 buckets per logical bucket key.
@@ -41,7 +41,7 @@ export declare function createBucketNameResolver(): BucketNameResolver;
41
41
  * (argument order: `(bucketKey, databaseId)`).
42
42
  *
43
43
  * Produces the exact same physical name as createBucketNameResolver()
44
- * (`{prefix}-{bucketKey}-{databaseId}`) so the eager `provisionBucket`
44
+ * (`{prefix}-{bucketKey}-{digest}`) so the eager `provisionBucket`
45
45
  * mutation mints the identical tenant-aware name that the lazy first-upload
46
46
  * path would. Throws on a missing prefix — no default bucket name.
47
47
  */
@@ -14,6 +14,7 @@ import { BucketProvisioner } from '@constructive-io/bucket-provisioner';
14
14
  import { getEnvOptions } from '@constructive-io/graphql-env';
15
15
  import { createS3Client } from '@constructive-io/s3-utils';
16
16
  import { Logger } from '@pgpmjs/logger';
17
+ import { createHash } from 'crypto';
17
18
  import { getBucketProvisionerConnection } from './bucket-provisioner-resolver';
18
19
  const log = new Logger('presigned-url-resolver');
19
20
  let s3Config = null;
@@ -79,21 +80,65 @@ function getBucketNamePrefix() {
79
80
  }
80
81
  return prefix;
81
82
  }
83
+ /** S3's hard ceiling on a bucket name. */
84
+ const MAX_BUCKET_NAME_LENGTH = 63;
85
+ /** S3's floor, which a degenerate prefix/key could otherwise fall under. */
86
+ const MIN_BUCKET_NAME_LENGTH = 3;
87
+ /** Hex characters of the identity digest kept as the uniqueness tail. */
88
+ const IDENTITY_DIGEST_LENGTH = 12;
89
+ /** Readable budget: how much of the name the prefix and key may each occupy. */
90
+ const PREFIX_BUDGET = 20;
91
+ const BUCKET_KEY_BUDGET = 63 - IDENTITY_DIGEST_LENGTH - PREFIX_BUDGET - 3;
82
92
  /**
83
- * The single physical-bucket naming policy: `{prefix}-{bucketKey}-{databaseId}`
84
- * (e.g., "myapp-public-abc123def456"). Both the presigned-upload (lazy) path
85
- * and the bucket-provisioner (eager) path derive names from this one function
86
- * so a bucket's physical name is identical regardless of which path mints it.
93
+ * Reduce a component to the S3 bucket-name alphabet: lowercase, `[a-z0-9-]`,
94
+ * with runs of separators collapsed and no leading or trailing hyphen.
95
+ *
96
+ * Dots are legal in a bucket name but deliberately dropped — a dotted name
97
+ * cannot be used with virtual-hosted-style HTTPS, because the wildcard
98
+ * certificate does not match a further label.
99
+ */
100
+ function sanitizeBucketNameComponent(value) {
101
+ return value
102
+ .toLowerCase()
103
+ .replace(/[^a-z0-9]+/g, '-')
104
+ .replace(/^-+|-+$/g, '');
105
+ }
106
+ /**
107
+ * The single physical-bucket naming policy:
108
+ * `{prefix}-{bucketKey}-{digest}` (e.g. `myapp-public-3f9c1a2b7e04`).
109
+ *
110
+ * Both the presigned-upload (lazy) path and the bucket-provisioner (eager) path
111
+ * derive names from this one function, so a bucket's physical name is identical
112
+ * regardless of which path mints it.
113
+ *
114
+ * The name is bounded and S3-legal by construction: the prefix and key are
115
+ * sanitized to `[a-z0-9-]` and truncated to a readable budget, and the tail is a
116
+ * digest of the *untruncated* identity — so two buckets whose keys agree only
117
+ * past the truncation point, or the same key in two databases, still get distinct
118
+ * names. Names remain stable for a given (prefix, databaseId, bucketKey) because
119
+ * nothing here reads the clock or a counter; and an already-provisioned bucket
120
+ * never consults this function at all, since `platform_buckets.physical_name` is
121
+ * authoritative once recorded.
87
122
  */
88
123
  function mintPhysicalBucketName(prefix, databaseId, bucketKey) {
89
- return `${prefix}-${bucketKey}-${databaseId}`;
124
+ const identity = `${prefix}/${databaseId}/${bucketKey}`;
125
+ const digest = createHash('sha256').update(identity).digest('hex').slice(0, IDENTITY_DIGEST_LENGTH);
126
+ const safePrefix = sanitizeBucketNameComponent(prefix).slice(0, PREFIX_BUDGET).replace(/-+$/, '');
127
+ const safeKey = sanitizeBucketNameComponent(bucketKey).slice(0, BUCKET_KEY_BUDGET).replace(/-+$/, '');
128
+ const name = [safePrefix, safeKey, digest].filter((part) => part.length > 0).join('-');
129
+ // The digest alone already satisfies both bounds, so this is only reachable
130
+ // when both readable components sanitize away to nothing.
131
+ if (name.length < MIN_BUCKET_NAME_LENGTH || name.length > MAX_BUCKET_NAME_LENGTH) {
132
+ throw new Error(`[presigned-url-resolver] Cannot mint a legal S3 bucket name for key "${bucketKey}": got "${name}"`);
133
+ }
134
+ return name;
90
135
  }
91
136
  /**
92
137
  * Create a per-(database, bucketKey) bucket name resolver for the presigned
93
138
  * URL plugin (argument order: `(databaseId, bucketKey)`).
94
139
  *
95
140
  * Uses CDN_BUCKET_NAME as a prefix. For each (database, bucketKey) pair, the
96
- * S3 bucket name becomes `{prefix}-{bucketKey}-{databaseId}`.
141
+ * S3 bucket name becomes `{prefix}-{bucketKey}-{digest}`.
97
142
  *
98
143
  * This aligns with the bucket provisioner plugin which creates separate
99
144
  * S3 buckets per logical bucket key.
@@ -107,7 +152,7 @@ export function createBucketNameResolver() {
107
152
  * (argument order: `(bucketKey, databaseId)`).
108
153
  *
109
154
  * Produces the exact same physical name as createBucketNameResolver()
110
- * (`{prefix}-{bucketKey}-{databaseId}`) so the eager `provisionBucket`
155
+ * (`{prefix}-{bucketKey}-{digest}`) so the eager `provisionBucket`
111
156
  * mutation mints the identical tenant-aware name that the lazy first-upload
112
157
  * path would. Throws on a missing prefix — no default bucket name.
113
158
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphile-settings",
3
- "version": "6.18.0",
3
+ "version": "6.18.1",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "graphile settings",
6
6
  "main": "index.js",
@@ -59,7 +59,7 @@
59
59
  "graphile-meta": "^1.8.0",
60
60
  "graphile-pg-aggregates": "^2.13.0",
61
61
  "graphile-postgis": "^3.14.0",
62
- "graphile-presigned-url-plugin": "^1.14.0",
62
+ "graphile-presigned-url-plugin": "^1.14.1",
63
63
  "graphile-realtime-subscriptions": "^1.11.0",
64
64
  "graphile-search": "^2.13.0",
65
65
  "graphile-upload-plugin": "^2.26.0",
@@ -92,5 +92,5 @@
92
92
  "constructive",
93
93
  "graphql"
94
94
  ],
95
- "gitHead": "b5b6b9f05276b7e9cbd3d1f4acd31dd315c353ea"
95
+ "gitHead": "8d77dfe57bb328cbdfe10732b10ca24865adf9c0"
96
96
  }
@@ -30,7 +30,7 @@ export declare function getPresignedUrlS3Config(): S3Config;
30
30
  * URL plugin (argument order: `(databaseId, bucketKey)`).
31
31
  *
32
32
  * Uses CDN_BUCKET_NAME as a prefix. For each (database, bucketKey) pair, the
33
- * S3 bucket name becomes `{prefix}-{bucketKey}-{databaseId}`.
33
+ * S3 bucket name becomes `{prefix}-{bucketKey}-{digest}`.
34
34
  *
35
35
  * This aligns with the bucket provisioner plugin which creates separate
36
36
  * S3 buckets per logical bucket key.
@@ -41,7 +41,7 @@ export declare function createBucketNameResolver(): BucketNameResolver;
41
41
  * (argument order: `(bucketKey, databaseId)`).
42
42
  *
43
43
  * Produces the exact same physical name as createBucketNameResolver()
44
- * (`{prefix}-{bucketKey}-{databaseId}`) so the eager `provisionBucket`
44
+ * (`{prefix}-{bucketKey}-{digest}`) so the eager `provisionBucket`
45
45
  * mutation mints the identical tenant-aware name that the lazy first-upload
46
46
  * path would. Throws on a missing prefix — no default bucket name.
47
47
  */
@@ -21,6 +21,7 @@ const bucket_provisioner_1 = require("@constructive-io/bucket-provisioner");
21
21
  const graphql_env_1 = require("@constructive-io/graphql-env");
22
22
  const s3_utils_1 = require("@constructive-io/s3-utils");
23
23
  const logger_1 = require("@pgpmjs/logger");
24
+ const crypto_1 = require("crypto");
24
25
  const bucket_provisioner_resolver_1 = require("./bucket-provisioner-resolver");
25
26
  const log = new logger_1.Logger('presigned-url-resolver');
26
27
  let s3Config = null;
@@ -86,21 +87,65 @@ function getBucketNamePrefix() {
86
87
  }
87
88
  return prefix;
88
89
  }
90
+ /** S3's hard ceiling on a bucket name. */
91
+ const MAX_BUCKET_NAME_LENGTH = 63;
92
+ /** S3's floor, which a degenerate prefix/key could otherwise fall under. */
93
+ const MIN_BUCKET_NAME_LENGTH = 3;
94
+ /** Hex characters of the identity digest kept as the uniqueness tail. */
95
+ const IDENTITY_DIGEST_LENGTH = 12;
96
+ /** Readable budget: how much of the name the prefix and key may each occupy. */
97
+ const PREFIX_BUDGET = 20;
98
+ const BUCKET_KEY_BUDGET = 63 - IDENTITY_DIGEST_LENGTH - PREFIX_BUDGET - 3;
89
99
  /**
90
- * The single physical-bucket naming policy: `{prefix}-{bucketKey}-{databaseId}`
91
- * (e.g., "myapp-public-abc123def456"). Both the presigned-upload (lazy) path
92
- * and the bucket-provisioner (eager) path derive names from this one function
93
- * so a bucket's physical name is identical regardless of which path mints it.
100
+ * Reduce a component to the S3 bucket-name alphabet: lowercase, `[a-z0-9-]`,
101
+ * with runs of separators collapsed and no leading or trailing hyphen.
102
+ *
103
+ * Dots are legal in a bucket name but deliberately dropped — a dotted name
104
+ * cannot be used with virtual-hosted-style HTTPS, because the wildcard
105
+ * certificate does not match a further label.
106
+ */
107
+ function sanitizeBucketNameComponent(value) {
108
+ return value
109
+ .toLowerCase()
110
+ .replace(/[^a-z0-9]+/g, '-')
111
+ .replace(/^-+|-+$/g, '');
112
+ }
113
+ /**
114
+ * The single physical-bucket naming policy:
115
+ * `{prefix}-{bucketKey}-{digest}` (e.g. `myapp-public-3f9c1a2b7e04`).
116
+ *
117
+ * Both the presigned-upload (lazy) path and the bucket-provisioner (eager) path
118
+ * derive names from this one function, so a bucket's physical name is identical
119
+ * regardless of which path mints it.
120
+ *
121
+ * The name is bounded and S3-legal by construction: the prefix and key are
122
+ * sanitized to `[a-z0-9-]` and truncated to a readable budget, and the tail is a
123
+ * digest of the *untruncated* identity — so two buckets whose keys agree only
124
+ * past the truncation point, or the same key in two databases, still get distinct
125
+ * names. Names remain stable for a given (prefix, databaseId, bucketKey) because
126
+ * nothing here reads the clock or a counter; and an already-provisioned bucket
127
+ * never consults this function at all, since `platform_buckets.physical_name` is
128
+ * authoritative once recorded.
94
129
  */
95
130
  function mintPhysicalBucketName(prefix, databaseId, bucketKey) {
96
- return `${prefix}-${bucketKey}-${databaseId}`;
131
+ const identity = `${prefix}/${databaseId}/${bucketKey}`;
132
+ const digest = (0, crypto_1.createHash)('sha256').update(identity).digest('hex').slice(0, IDENTITY_DIGEST_LENGTH);
133
+ const safePrefix = sanitizeBucketNameComponent(prefix).slice(0, PREFIX_BUDGET).replace(/-+$/, '');
134
+ const safeKey = sanitizeBucketNameComponent(bucketKey).slice(0, BUCKET_KEY_BUDGET).replace(/-+$/, '');
135
+ const name = [safePrefix, safeKey, digest].filter((part) => part.length > 0).join('-');
136
+ // The digest alone already satisfies both bounds, so this is only reachable
137
+ // when both readable components sanitize away to nothing.
138
+ if (name.length < MIN_BUCKET_NAME_LENGTH || name.length > MAX_BUCKET_NAME_LENGTH) {
139
+ throw new Error(`[presigned-url-resolver] Cannot mint a legal S3 bucket name for key "${bucketKey}": got "${name}"`);
140
+ }
141
+ return name;
97
142
  }
98
143
  /**
99
144
  * Create a per-(database, bucketKey) bucket name resolver for the presigned
100
145
  * URL plugin (argument order: `(databaseId, bucketKey)`).
101
146
  *
102
147
  * Uses CDN_BUCKET_NAME as a prefix. For each (database, bucketKey) pair, the
103
- * S3 bucket name becomes `{prefix}-{bucketKey}-{databaseId}`.
148
+ * S3 bucket name becomes `{prefix}-{bucketKey}-{digest}`.
104
149
  *
105
150
  * This aligns with the bucket provisioner plugin which creates separate
106
151
  * S3 buckets per logical bucket key.
@@ -114,7 +159,7 @@ function createBucketNameResolver() {
114
159
  * (argument order: `(bucketKey, databaseId)`).
115
160
  *
116
161
  * Produces the exact same physical name as createBucketNameResolver()
117
- * (`{prefix}-{bucketKey}-{databaseId}`) so the eager `provisionBucket`
162
+ * (`{prefix}-{bucketKey}-{digest}`) so the eager `provisionBucket`
118
163
  * mutation mints the identical tenant-aware name that the lazy first-upload
119
164
  * path would. Throws on a missing prefix — no default bucket name.
120
165
  */