===
+// ---------------------------------------------------------------------------
+// Static file serving
+// ---------------------------------------------------------------------------
app.get('/', (req, res) => {
- res.setHeader('Content-Type', 'text/html')
const htmlPath = path.join(__dirname, 'public', 'index.html')
- res.sendFile(htmlPath)
+ require('node:fs').readFile(htmlPath, 'utf8', (err, html) => {
+ if (err) return res.status(500).send('Error loading page')
+ // Inject bucket/region config so the client can read them.
+ const config = ``
+ res.setHeader('Content-Type', 'text/html')
+ res.send(html.replace('', `${config}`))
+ })
})
app.get('/index.html', (req, res) => {
res.setHeader('Location', '/').sendStatus(308).end()
})
-app.get('/withCustomEndpoints.html', (req, res) => {
- res.setHeader('Content-Type', 'text/html')
- const htmlPath = path.join(__dirname, 'public', 'withCustomEndpoints.html')
- res.sendFile(htmlPath)
-})
-app.get('/rewrite-test.html', (req, res) => {
- res.setHeader('Content-Type', 'text/html')
- // Inject bucket config as JS variables
- const config = ``
- const htmlPath = path.join(__dirname, 'public', 'rewrite-test.html')
- require('node:fs').readFile(htmlPath, 'utf8', (err, html) => {
- if (err) return res.status(500).send('Error loading page')
- // Inject config before
- const modifiedHtml = html.replace('', `${config}`)
- res.send(modifiedHtml)
- })
-})
app.get('/uppy.min.mjs', (req, res) => {
res.setHeader('Content-Type', 'text/javascript')
diff --git a/examples/aws-nodejs/public/index.html b/examples/aws-nodejs/public/index.html
index 881a07567..d3a09d7b5 100644
--- a/examples/aws-nodejs/public/index.html
+++ b/examples/aws-nodejs/public/index.html
@@ -2,69 +2,117 @@
- Uppy – AWS upload example
+ Uppy – AWS S3 upload example
+
- AWS upload example
+ AWS S3 upload example
+ Demonstrates the @uppy/aws-s3 plugin with two signing modes.
+
- Sign on the server
-
+ Sign on the client (STS credentials)
+
+ Uses getCredentials to fetch temporary STS credentials from
+ /s3/sts. The browser signs all S3 requests locally using SigV4.
+
+
+
- Sign on the client (if WebCrypto is available)
-
+ Sign on the server (presigned URLs)
+
+ Uses signRequest to call /s3/presign for each S3
+ operation. The server generates presigned URLs; the browser uses them directly.
+
+
-
+
diff --git a/examples/aws-nodejs/public/rewrite-test.html b/examples/aws-nodejs/public/rewrite-test.html
deleted file mode 100644
index 350c19370..000000000
--- a/examples/aws-nodejs/public/rewrite-test.html
+++ /dev/null
@@ -1,126 +0,0 @@
-
-
-
-
- Uppy – AWS S3 Plugin Rewrite Test
-
-
-
-
- AWS S3 Plugin Rewrite Test
- Testing the rewritten @uppy/aws-s3 plugin using S3mini.
-
-
- Test 1: Using getCredentials (STS)
-
-
Uses the /s3/sts endpoint to get temporary credentials. S3mini handles signing internally.
-
-
-
-
-
- Test 2: Using signRequest (Server Signing)
-
-
Uses the /s3/presign endpoint to sign each request on the server.
-
-
-
-
-
-
-
-
-
-
diff --git a/examples/aws-nodejs/public/withCustomEndpoints.html b/examples/aws-nodejs/public/withCustomEndpoints.html
deleted file mode 100644
index 2265a7bb7..000000000
--- a/examples/aws-nodejs/public/withCustomEndpoints.html
+++ /dev/null
@@ -1,267 +0,0 @@
-
-
-
-
- Uppy – AWS upload example
-
-
-
- AWS upload example
-
-
-
-
-
-
diff --git a/examples/aws-nodejs/routes/presign.js b/examples/aws-nodejs/routes/presign.js
new file mode 100644
index 000000000..a1f4638ac
--- /dev/null
+++ b/examples/aws-nodejs/routes/presign.js
@@ -0,0 +1,108 @@
+/**
+ * POST /s3/presign — Presigned URLs for server-side signing (signRequest)
+ *
+ * The client sends the S3 operation details (method, key, uploadId, etc.)
+ * and this endpoint returns a presigned URL. The browser then sends the
+ * actual request directly to S3 using that URL.
+ */
+
+const { Router } = require('express')
+const {
+ S3Client,
+ AbortMultipartUploadCommand,
+ CompleteMultipartUploadCommand,
+ CreateMultipartUploadCommand,
+ ListPartsCommand,
+ PutObjectCommand,
+ UploadPartCommand,
+} = require('@aws-sdk/client-s3')
+const { getSignedUrl } = require('@aws-sdk/s3-request-presigner')
+
+const expiresIn = 900 // 15 minutes
+
+let s3Client
+function getS3Client() {
+ s3Client ??= new S3Client({
+ region: process.env.COMPANION_AWS_REGION,
+ credentials: {
+ accessKeyId: process.env.COMPANION_AWS_KEY,
+ secretAccessKey: process.env.COMPANION_AWS_SECRET,
+ },
+ forcePathStyle: process.env.COMPANION_AWS_FORCE_PATH_STYLE === 'true',
+ })
+ return s3Client
+}
+
+const router = Router()
+
+router.post('/s3/presign', async (req, res, next) => {
+ // Before giving the presigned URL to the client, you should first check if
+ // they are authorized to perform that operation, and if the request is legit.
+ // For the sake of simplification, we skip that check in this example.
+
+ try {
+ const { method, key, uploadId, partNumber, contentType } = req.body
+ const client = getS3Client()
+ const bucket = process.env.COMPANION_AWS_BUCKET
+
+ if (!method || !key) {
+ return res.status(400).json({ error: 'method and key are required' })
+ }
+
+ let command
+
+ if (method === 'PUT' && uploadId && partNumber) {
+ // UploadPart (multipart)
+ command = new UploadPartCommand({
+ Bucket: bucket,
+ Key: key,
+ UploadId: uploadId,
+ PartNumber: parseInt(partNumber, 10),
+ })
+ } else if (method === 'PUT' && !uploadId && !partNumber) {
+ // PutObject (simple upload)
+ command = new PutObjectCommand({
+ Bucket: bucket,
+ Key: key,
+ ContentType: contentType || 'application/octet-stream',
+ })
+ } else if (method === 'POST' && !uploadId) {
+ // CreateMultipartUpload
+ command = new CreateMultipartUploadCommand({
+ Bucket: bucket,
+ Key: key,
+ ContentType: contentType || 'application/octet-stream',
+ })
+ } else if (method === 'POST' && uploadId) {
+ // CompleteMultipartUpload
+ command = new CompleteMultipartUploadCommand({
+ Bucket: bucket,
+ Key: key,
+ UploadId: uploadId,
+ })
+ } else if (method === 'DELETE' && uploadId) {
+ // AbortMultipartUpload
+ command = new AbortMultipartUploadCommand({
+ Bucket: bucket,
+ Key: key,
+ UploadId: uploadId,
+ })
+ } else if (method === 'GET' && uploadId) {
+ // ListParts
+ command = new ListPartsCommand({
+ Bucket: bucket,
+ Key: key,
+ UploadId: uploadId,
+ })
+ } else {
+ return res.status(400).json({ error: 'Unsupported operation' })
+ }
+
+ const url = await getSignedUrl(client, command, { expiresIn })
+ res.json({ url })
+ } catch (err) {
+ next(err)
+ }
+})
+
+module.exports = router
diff --git a/examples/aws-nodejs/routes/sts.js b/examples/aws-nodejs/routes/sts.js
new file mode 100644
index 000000000..b758eca63
--- /dev/null
+++ b/examples/aws-nodejs/routes/sts.js
@@ -0,0 +1,65 @@
+/**
+ * GET /s3/sts — Temporary credentials for client-side signing (getCredentials)
+ *
+ * Returns short-lived STS credentials so the browser can sign S3 requests
+ * locally using SigV4. The credentials are scoped to PutObject only.
+ */
+
+const { Router } = require('express')
+const { STSClient, GetFederationTokenCommand } = require('@aws-sdk/client-sts')
+
+const expiresIn = 900 // 15 minutes
+
+// IAM policy for the federated user — allows PutObject to the bucket.
+const policy = {
+ Version: '2012-10-17',
+ Statement: [
+ {
+ Effect: 'Allow',
+ Action: ['s3:PutObject'],
+ Resource: [
+ `arn:aws:s3:::${process.env.COMPANION_AWS_BUCKET}/*`,
+ `arn:aws:s3:::${process.env.COMPANION_AWS_BUCKET}`,
+ ],
+ },
+ ],
+}
+
+let stsClient
+function getSTSClient() {
+ stsClient ??= new STSClient({
+ region: process.env.COMPANION_AWS_REGION,
+ credentials: {
+ accessKeyId: process.env.COMPANION_AWS_KEY,
+ secretAccessKey: process.env.COMPANION_AWS_SECRET,
+ },
+ })
+ return stsClient
+}
+
+const router = Router()
+
+router.get('/s3/sts', (req, res, next) => {
+ // Before giving the STS token to the client, you should first check if they
+ // are authorized to perform that operation, and if the request is legit.
+ // For the sake of simplification, we skip that check in this example.
+
+ getSTSClient()
+ .send(
+ new GetFederationTokenCommand({
+ Name: '123user',
+ DurationSeconds: expiresIn,
+ Policy: JSON.stringify(policy),
+ }),
+ )
+ .then((response) => {
+ res.setHeader('Cache-Control', `public,max-age=${expiresIn}`)
+ res.json({
+ credentials: response.Credentials,
+ bucket: process.env.COMPANION_AWS_BUCKET,
+ region: process.env.COMPANION_AWS_REGION,
+ })
+ }, next)
+})
+
+module.exports = router