s3broker 0.4.4 → 0.4.5
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/README.md +9 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,12 +50,14 @@ export default {
|
|
|
50
50
|
|
|
51
51
|
## With Custom Guardrails
|
|
52
52
|
|
|
53
|
+
Example: Reject requests deleting files older than 1 hour unless the file has path prefix `/frequent_updated/`.
|
|
54
|
+
|
|
53
55
|
```typescript
|
|
54
56
|
import { handle } from 's3broker';
|
|
55
57
|
|
|
56
58
|
export default {
|
|
57
59
|
async fetch(request, env, ctx) {
|
|
58
|
-
return handle(request, {
|
|
60
|
+
return handle(request, ctx, {
|
|
59
61
|
s3Endpoint: env.S3_ENDPOINT,
|
|
60
62
|
clientAccessKeyId: env.CLIENT_ACCESS_KEY_ID,
|
|
61
63
|
clientSecretAccessKey: env.CLIENT_SECRET_ACCESS_KEY,
|
|
@@ -64,8 +66,12 @@ export default {
|
|
|
64
66
|
guardrailConfig: {
|
|
65
67
|
noDeleteOld: [
|
|
66
68
|
{
|
|
67
|
-
pattern: '/
|
|
68
|
-
config:
|
|
69
|
+
pattern: '/frequent_updated/.*',
|
|
70
|
+
config: null,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
pattern: '/.*',
|
|
74
|
+
config: { noDeleteBeforeSeconds: 3600 },
|
|
69
75
|
},
|
|
70
76
|
],
|
|
71
77
|
},
|
package/dist/index.d.mts
CHANGED
|
@@ -8,9 +8,9 @@ type GuardrailViolation = {
|
|
|
8
8
|
declare const GuardrailConfig: z.ZodObject<{
|
|
9
9
|
noDeleteOld: z.ZodArray<z.ZodObject<{
|
|
10
10
|
pattern: z.ZodString;
|
|
11
|
-
config: z.ZodObject<{
|
|
11
|
+
config: z.ZodNullable<z.ZodObject<{
|
|
12
12
|
noDeleteBeforeSeconds: z.ZodNumber;
|
|
13
|
-
}, z.core.$strip
|
|
13
|
+
}, z.core.$strip>>;
|
|
14
14
|
}, z.core.$strip>>;
|
|
15
15
|
}, z.core.$strip>;
|
|
16
16
|
type GuardrailConfig = z.infer<typeof GuardrailConfig>;
|
|
@@ -89,5 +89,5 @@ declare const defaultGuardrailConfig: GuardrailConfig;
|
|
|
89
89
|
declare function handle(request: Request<unknown, IncomingRequestCfProperties>, options: S3BrokerOptions): Promise<Response>;
|
|
90
90
|
//# sourceMappingURL=index.d.ts.map
|
|
91
91
|
//#endregion
|
|
92
|
-
export { type GuardrailConfig, type GuardrailViolation, type S3BrokerOptions, defaultGuardrailConfig, handle };
|
|
92
|
+
export { type GuardrailConfig, GuardrailConfig as GuardrailConfigZod, type GuardrailViolation, type S3BrokerOptions, defaultGuardrailConfig, handle };
|
|
93
93
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/guardrails/type.ts","../src/types.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/guardrails/type.ts","../src/types.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;AAsBa,KAfD,kBAAA,GAiBV;EAAA,SAAA,EAAA,MAAA;CAAA;cAFW,iBAAe,CAAA,CAAA;;aAE1B,CAAA,CAAA;;MAF0B,qBAAA,aAAA;IAAA,CAAA,eAAA,CAAA,CAAA;EAGhB,CAAA,eAAA,CAAA,CAAe;CAAA,eAAA,CAAA;AAAkB,KAAjC,eAAA,GAAkB,CAAA,CAAE,KAAa,CAAA,OAAA,eAAA,CAAA;;AAAR;;;;;;AAlBrC;AAeA;;;;UCbiB,eAAA;;;;;;;;ADaW;EAGhB,UAAA,EAAA,MAAe;EAAA;;;AAAU;;;;AChBrC;;;;AC0EA;AA+BA;;EAA4B,mBAA2B,EAAA,MAAA;EAA2B;;;;EAAqC,uBAAA,EAAA,MAAA;;;;;oBDlEpG;;;;;ADvBW,cE0DjB,sBF1DmB,EE0DK,eF1DL;AAAK;;;;AChBrC;;;;AC0EA;AA+BA;;;;;;;AAAuH;;;;iBAAjG,MAAA,UAAgB,iBAAiB,uCAAuC,kBAAkB,QAAQ"}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,9 +8,9 @@ type GuardrailViolation = {
|
|
|
8
8
|
declare const GuardrailConfig: z.ZodObject<{
|
|
9
9
|
noDeleteOld: z.ZodArray<z.ZodObject<{
|
|
10
10
|
pattern: z.ZodString;
|
|
11
|
-
config: z.ZodObject<{
|
|
11
|
+
config: z.ZodNullable<z.ZodObject<{
|
|
12
12
|
noDeleteBeforeSeconds: z.ZodNumber;
|
|
13
|
-
}, z.core.$strip
|
|
13
|
+
}, z.core.$strip>>;
|
|
14
14
|
}, z.core.$strip>>;
|
|
15
15
|
}, z.core.$strip>;
|
|
16
16
|
type GuardrailConfig = z.infer<typeof GuardrailConfig>;
|
|
@@ -89,5 +89,5 @@ declare const defaultGuardrailConfig: GuardrailConfig;
|
|
|
89
89
|
declare function handle(request: Request<unknown, IncomingRequestCfProperties>, options: S3BrokerOptions): Promise<Response>;
|
|
90
90
|
//# sourceMappingURL=index.d.ts.map
|
|
91
91
|
//#endregion
|
|
92
|
-
export { type GuardrailConfig, type GuardrailViolation, type S3BrokerOptions, defaultGuardrailConfig, handle };
|
|
92
|
+
export { type GuardrailConfig, GuardrailConfig as GuardrailConfigZod, type GuardrailViolation, type S3BrokerOptions, defaultGuardrailConfig, handle };
|
|
93
93
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/guardrails/type.ts","../src/types.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/guardrails/type.ts","../src/types.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;AAsBa,KAfD,kBAAA,GAiBV;EAAA,SAAA,EAAA,MAAA;CAAA;cAFW,iBAAe,CAAA,CAAA;;aAE1B,CAAA,CAAA;;MAF0B,qBAAA,aAAA;IAAA,CAAA,eAAA,CAAA,CAAA;EAGhB,CAAA,eAAA,CAAA,CAAe;CAAA,eAAA,CAAA;AAAkB,KAAjC,eAAA,GAAkB,CAAA,CAAE,KAAa,CAAA,OAAA,eAAA,CAAA;;AAAR;;;;;;AAlBrC;AAeA;;;;UCbiB,eAAA;;;;;;;;ADaW;EAGhB,UAAA,EAAA,MAAe;EAAA;;;AAAU;;;;AChBrC;;;;AC0EA;AA+BA;;EAA4B,mBAA2B,EAAA,MAAA;EAA2B;;;;EAAqC,uBAAA,EAAA,MAAA;;;;;oBDlEpG;;;;;ADvBW,cE0DjB,sBF1DmB,EE0DK,eF1DL;AAAK;;;;AChBrC;;;;AC0EA;AA+BA;;;;;;;AAAuH;;;;iBAAjG,MAAA,UAAgB,iBAAiB,uCAAuC,kBAAkB,QAAQ"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));const c=require(`./sigv4-CT4jkHNc.js`),l=s(require(`aws4fetch`)),u=s(require(`zod`));let d=function(e){return e[e.Forbidden=403]=`Forbidden`,e[e.UpstreamFailure=502]=`UpstreamFailure`,e}({});function f(e,t){return new Response(e,{status:t,headers:{"Content-Type":`text/plain`}})}async function p(e,t){let n=await e.fetch(t,{method:`HEAD`});if(!n.ok)throw new v(n.status.toString(),n.statusText);return n.headers}const m=u.z.object({noDeleteBeforeSeconds:u.z.number().int()});var h=class{config;upstreamFetcher;upstreamEndpoint;currentTimestampMs;constructor(e,t,n,r){this.config=e,this.upstreamFetcher=t,this.upstreamEndpoint=n,this.currentTimestampMs=r}async evaluate(e){if(e.method!==`DELETE`)return null;let t=new URL(e.url),n=this.upstreamEndpoint+t.pathname;try{let e=await p(this.upstreamFetcher,n),t=e.get(`Last-Modified`);if(!t)return null;let r=new Date(t).getTime(),i=this.currentTimestampMs-r,a=this.config.noDeleteBeforeSeconds*1e3;return i>a?{violation:`Cannot delete object: object is ${Math.floor(i/1e3)} seconds old, which exceeds the ${this.config.noDeleteBeforeSeconds} seconds threshold`}:null}catch(e){if(e instanceof v&&e.code===`404`)return null;throw e}}};const g=e=>u.z.array(u.z.object({pattern:u.z.string(),config:e})),_=u.z.object({noDeleteOld:g(m)});var v=class extends Error{code;constructor(e,t){super(t),this.code=e}};async function y(e,t,n,r,i){let a=new URL(e.url).pathname,o=b(i,a,t,n,r);if(o.length===0)return null;let s=o.map(({name:t,policy:n})=>({policyName:t,promise:(async()=>{let r=await n.evaluate(e);return r?{...r,policy:t}:null})()}));return new Promise(e=>{let t=s.length;for(let{policyName:n,promise:r}of s)r.then(n=>{n===null?(t--,t===0&&e(null)):e(n)}).catch(t=>{let r=t instanceof Error?t.message:String(t);t instanceof v&&(r=`Error calling upstream when evaluating guardrail: ${r}`),e({violation:r,policy:n})})})}function b(e,t,n,r,i){let a=[];for(let o of e.noDeleteOld){let e=new RegExp(o.pattern);if(e.test(t)){a.push({name:`noDeleteOld`,policy:new h(o.config,n,r,i)});break}}return a}const x=new Set(`x-amz-date.x-amz-content-sha256.x-amz-security-token.x-amz-server-side-encryption.x-amz-server-side-encryption-aws-kms-key-id.x-amz-server-side-encryption-customer-algorithm.x-amz-server-side-encryption-customer-key.x-amz-server-side-encryption-customer-key-md5.x-amz-storage-class.x-amz-tagging.x-amz-website-redirect-location.x-amz-acl.x-amz-grant-read.x-amz-grant-write.x-amz-grant-read-acp.x-amz-grant-write-acp.x-amz-grant-full-control.x-amz-metadata-directive.x-amz-copy-source.x-amz-copy-source-if-match.x-amz-copy-source-if-none-match.x-amz-copy-source-if-unmodified-since.x-amz-copy-source-if-modified-since.x-amz-copy-source-range.content-type.content-length.content-md5.content-encoding.content-disposition.cache-control.expires.range.if-match.if-none-match.if-modified-since.if-unmodified-since.user-agent`.split(`.`)),S=new Set([`X-Amz-Algorithm`,`X-Amz-Credential`,`X-Amz-Date`,`X-Amz-Expires`,`X-Amz-SignedHeaders`,`X-Amz-Signature`,`X-Amz-Security-Token`]),C={noDeleteOld:[{pattern:`/.*`,config:{noDeleteBeforeSeconds:60}}]};async function w(e,t){let n=Date.now(),r=await c.verifySignature(e,t.clientSecretAccessKey,t.clientAccessKeyId,n);if(!r.valid)return f(`Signature verification failed: ${r.error}`,d.Forbidden);let i=new URL(e.url),a=new l.AwsClient({accessKeyId:t.upstreamAccessKeyId,secretAccessKey:t.upstreamSecretAccessKey,retries:5}),o=await y(e,a,t.s3Endpoint,n,t.guardrailConfig||C);if(o)return console.log(`Guardrail violation`,{path:i.pathname,method:e.method,query:i.search,policy:o.policy,violation:o.violation}),f(`Request violating guardrail policy ${o.policy}: ${o.violation}`,d.Forbidden);let s=new URL(i.pathname,t.s3Endpoint);for(let[e,t]of i.searchParams.entries())S.has(e)||s.searchParams.set(e,t);let u=new Headers;for(let[t,n]of e.headers.entries())x.has(t.toLowerCase())&&u.set(t,n);u.set(`x-amz-content-sha256`,`UNSIGNED-PAYLOAD`);let p=new Request(s.toString(),{method:e.method,headers:u,body:e.body,duplex:`half`}),m=new l.AwsClient({accessKeyId:t.upstreamAccessKeyId,secretAccessKey:t.upstreamSecretAccessKey,retries:0});try{return await m.fetch(p)}catch(e){return console.error(`Upstream request failed:`,e),f(`Upstream request failed: ${e instanceof Error?e.message:`Unknown error`}`,d.UpstreamFailure)}}exports.defaultGuardrailConfig=C,exports.handle=w;
|
|
1
|
+
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));const c=require(`./sigv4-CT4jkHNc.js`),l=s(require(`aws4fetch`)),u=s(require(`zod`));let d=function(e){return e[e.Forbidden=403]=`Forbidden`,e[e.UpstreamFailure=502]=`UpstreamFailure`,e}({});function f(e,t){return new Response(e,{status:t,headers:{"Content-Type":`text/plain`}})}async function p(e,t){let n=await e.fetch(t,{method:`HEAD`});if(!n.ok)throw new v(n.status.toString(),n.statusText);return n.headers}const m=u.z.object({noDeleteBeforeSeconds:u.z.number().int()});var h=class{config;upstreamFetcher;upstreamEndpoint;currentTimestampMs;constructor(e,t,n,r){this.config=e,this.upstreamFetcher=t,this.upstreamEndpoint=n,this.currentTimestampMs=r}async evaluate(e){if(e.method!==`DELETE`)return null;let t=new URL(e.url),n=this.upstreamEndpoint+t.pathname;try{let e=await p(this.upstreamFetcher,n),t=e.get(`Last-Modified`);if(!t)return null;let r=new Date(t).getTime(),i=this.currentTimestampMs-r,a=this.config.noDeleteBeforeSeconds*1e3;return i>a?{violation:`Cannot delete object: object is ${Math.floor(i/1e3)} seconds old, which exceeds the ${this.config.noDeleteBeforeSeconds} seconds threshold`}:null}catch(e){if(e instanceof v&&e.code===`404`)return null;throw e}}};const g=e=>u.z.array(u.z.object({pattern:u.z.string(),config:e.nullable()})),_=u.z.object({noDeleteOld:g(m)});var v=class extends Error{code;constructor(e,t){super(t),this.code=e}};async function y(e,t,n,r,i){let a=new URL(e.url).pathname,o=b(i,a,t,n,r);if(o.length===0)return null;let s=o.map(({name:t,policy:n})=>({policyName:t,promise:(async()=>{let r=await n.evaluate(e);return r?{...r,policy:t}:null})()}));return new Promise(e=>{let t=s.length;for(let{policyName:n,promise:r}of s)r.then(n=>{n===null?(t--,t===0&&e(null)):e(n)}).catch(t=>{let r=t instanceof Error?t.message:String(t);t instanceof v&&(r=`Error calling upstream when evaluating guardrail: ${r}`),e({violation:r,policy:n})})})}function b(e,t,n,r,i){let a=[];for(let o of e.noDeleteOld){let e=new RegExp(o.pattern);if(e.test(t)){o.config!==null&&a.push({name:`noDeleteOld`,policy:new h(o.config,n,r,i)});break}}return a}const x=new Set(`x-amz-date.x-amz-content-sha256.x-amz-security-token.x-amz-server-side-encryption.x-amz-server-side-encryption-aws-kms-key-id.x-amz-server-side-encryption-customer-algorithm.x-amz-server-side-encryption-customer-key.x-amz-server-side-encryption-customer-key-md5.x-amz-storage-class.x-amz-tagging.x-amz-website-redirect-location.x-amz-acl.x-amz-grant-read.x-amz-grant-write.x-amz-grant-read-acp.x-amz-grant-write-acp.x-amz-grant-full-control.x-amz-metadata-directive.x-amz-copy-source.x-amz-copy-source-if-match.x-amz-copy-source-if-none-match.x-amz-copy-source-if-unmodified-since.x-amz-copy-source-if-modified-since.x-amz-copy-source-range.content-type.content-length.content-md5.content-encoding.content-disposition.cache-control.expires.range.if-match.if-none-match.if-modified-since.if-unmodified-since.user-agent`.split(`.`)),S=new Set([`X-Amz-Algorithm`,`X-Amz-Credential`,`X-Amz-Date`,`X-Amz-Expires`,`X-Amz-SignedHeaders`,`X-Amz-Signature`,`X-Amz-Security-Token`]),C={noDeleteOld:[{pattern:`/.*`,config:{noDeleteBeforeSeconds:60}}]};async function w(e,t){let n=Date.now(),r=await c.verifySignature(e,t.clientSecretAccessKey,t.clientAccessKeyId,n);if(!r.valid)return f(`Signature verification failed: ${r.error}`,d.Forbidden);let i=new URL(e.url),a=new l.AwsClient({accessKeyId:t.upstreamAccessKeyId,secretAccessKey:t.upstreamSecretAccessKey,retries:5}),o=await y(e,a,t.s3Endpoint,n,t.guardrailConfig||C);if(o)return console.log(`Guardrail violation`,{path:i.pathname,method:e.method,query:i.search,policy:o.policy,violation:o.violation}),f(`Request violating guardrail policy ${o.policy}: ${o.violation}`,d.Forbidden);let s=new URL(i.pathname,t.s3Endpoint);for(let[e,t]of i.searchParams.entries())S.has(e)||s.searchParams.set(e,t);let u=new Headers;for(let[t,n]of e.headers.entries())x.has(t.toLowerCase())&&u.set(t,n);u.set(`x-amz-content-sha256`,`UNSIGNED-PAYLOAD`);let p=new Request(s.toString(),{method:e.method,headers:u,body:e.body,duplex:`half`}),m=new l.AwsClient({accessKeyId:t.upstreamAccessKeyId,secretAccessKey:t.upstreamSecretAccessKey,retries:0});try{return await m.fetch(p)}catch(e){return console.error(`Upstream request failed:`,e),f(`Upstream request failed: ${e instanceof Error?e.message:`Unknown error`}`,d.UpstreamFailure)}}exports.GuardrailConfigZod=_,exports.defaultGuardrailConfig=C,exports.handle=w;
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{verifySignature as e}from"./sigv4-Bh65MQC2.mjs";import{AwsClient as t}from"aws4fetch";import{z as n}from"zod";let r=function(e){return e[e.Forbidden=403]=`Forbidden`,e[e.UpstreamFailure=502]=`UpstreamFailure`,e}({});function i(e,t){return new Response(e,{status:t,headers:{"Content-Type":`text/plain`}})}async function a(e,t){let n=await e.fetch(t,{method:`HEAD`});if(!n.ok)throw new u(n.status.toString(),n.statusText);return n.headers}const o=n.object({noDeleteBeforeSeconds:n.number().int()});var s=class{config;upstreamFetcher;upstreamEndpoint;currentTimestampMs;constructor(e,t,n,r){this.config=e,this.upstreamFetcher=t,this.upstreamEndpoint=n,this.currentTimestampMs=r}async evaluate(e){if(e.method!==`DELETE`)return null;let t=new URL(e.url),n=this.upstreamEndpoint+t.pathname;try{let e=await a(this.upstreamFetcher,n),t=e.get(`Last-Modified`);if(!t)return null;let r=new Date(t).getTime(),i=this.currentTimestampMs-r,o=this.config.noDeleteBeforeSeconds*1e3;return i>o?{violation:`Cannot delete object: object is ${Math.floor(i/1e3)} seconds old, which exceeds the ${this.config.noDeleteBeforeSeconds} seconds threshold`}:null}catch(e){if(e instanceof u&&e.code===`404`)return null;throw e}}};const c=e=>n.array(n.object({pattern:n.string(),config:e})),l=n.object({noDeleteOld:c(o)});var u=class extends Error{code;constructor(e,t){super(t),this.code=e}};async function d(e,t,n,r,i){let a=new URL(e.url).pathname,o=f(i,a,t,n,r);if(o.length===0)return null;let s=o.map(({name:t,policy:n})=>({policyName:t,promise:(async()=>{let r=await n.evaluate(e);return r?{...r,policy:t}:null})()}));return new Promise(e=>{let t=s.length;for(let{policyName:n,promise:r}of s)r.then(n=>{n===null?(t--,t===0&&e(null)):e(n)}).catch(t=>{let r=t instanceof Error?t.message:String(t);t instanceof u&&(r=`Error calling upstream when evaluating guardrail: ${r}`),e({violation:r,policy:n})})})}function f(e,t,n,r,i){let a=[];for(let o of e.noDeleteOld){let e=new RegExp(o.pattern);if(e.test(t)){a.push({name:`noDeleteOld`,policy:new s(o.config,n,r,i)});break}}return a}const p=new Set(`x-amz-date.x-amz-content-sha256.x-amz-security-token.x-amz-server-side-encryption.x-amz-server-side-encryption-aws-kms-key-id.x-amz-server-side-encryption-customer-algorithm.x-amz-server-side-encryption-customer-key.x-amz-server-side-encryption-customer-key-md5.x-amz-storage-class.x-amz-tagging.x-amz-website-redirect-location.x-amz-acl.x-amz-grant-read.x-amz-grant-write.x-amz-grant-read-acp.x-amz-grant-write-acp.x-amz-grant-full-control.x-amz-metadata-directive.x-amz-copy-source.x-amz-copy-source-if-match.x-amz-copy-source-if-none-match.x-amz-copy-source-if-unmodified-since.x-amz-copy-source-if-modified-since.x-amz-copy-source-range.content-type.content-length.content-md5.content-encoding.content-disposition.cache-control.expires.range.if-match.if-none-match.if-modified-since.if-unmodified-since.user-agent`.split(`.`)),m=new Set([`X-Amz-Algorithm`,`X-Amz-Credential`,`X-Amz-Date`,`X-Amz-Expires`,`X-Amz-SignedHeaders`,`X-Amz-Signature`,`X-Amz-Security-Token`]),h={noDeleteOld:[{pattern:`/.*`,config:{noDeleteBeforeSeconds:60}}]};async function g(n,a){let o=Date.now(),s=await e(n,a.clientSecretAccessKey,a.clientAccessKeyId,o);if(!s.valid)return i(`Signature verification failed: ${s.error}`,r.Forbidden);let c=new URL(n.url),l=new t({accessKeyId:a.upstreamAccessKeyId,secretAccessKey:a.upstreamSecretAccessKey,retries:5}),u=await d(n,l,a.s3Endpoint,o,a.guardrailConfig||h);if(u)return console.log(`Guardrail violation`,{path:c.pathname,method:n.method,query:c.search,policy:u.policy,violation:u.violation}),i(`Request violating guardrail policy ${u.policy}: ${u.violation}`,r.Forbidden);let f=new URL(c.pathname,a.s3Endpoint);for(let[e,t]of c.searchParams.entries())m.has(e)||f.searchParams.set(e,t);let g=new Headers;for(let[e,t]of n.headers.entries())p.has(e.toLowerCase())&&g.set(e,t);g.set(`x-amz-content-sha256`,`UNSIGNED-PAYLOAD`);let _=new Request(f.toString(),{method:n.method,headers:g,body:n.body,duplex:`half`}),v=new t({accessKeyId:a.upstreamAccessKeyId,secretAccessKey:a.upstreamSecretAccessKey,retries:0});try{return await v.fetch(_)}catch(e){return console.error(`Upstream request failed:`,e),i(`Upstream request failed: ${e instanceof Error?e.message:`Unknown error`}`,r.UpstreamFailure)}}export{h as defaultGuardrailConfig,g as handle};
|
|
1
|
+
import{verifySignature as e}from"./sigv4-Bh65MQC2.mjs";import{AwsClient as t}from"aws4fetch";import{z as n}from"zod";let r=function(e){return e[e.Forbidden=403]=`Forbidden`,e[e.UpstreamFailure=502]=`UpstreamFailure`,e}({});function i(e,t){return new Response(e,{status:t,headers:{"Content-Type":`text/plain`}})}async function a(e,t){let n=await e.fetch(t,{method:`HEAD`});if(!n.ok)throw new u(n.status.toString(),n.statusText);return n.headers}const o=n.object({noDeleteBeforeSeconds:n.number().int()});var s=class{config;upstreamFetcher;upstreamEndpoint;currentTimestampMs;constructor(e,t,n,r){this.config=e,this.upstreamFetcher=t,this.upstreamEndpoint=n,this.currentTimestampMs=r}async evaluate(e){if(e.method!==`DELETE`)return null;let t=new URL(e.url),n=this.upstreamEndpoint+t.pathname;try{let e=await a(this.upstreamFetcher,n),t=e.get(`Last-Modified`);if(!t)return null;let r=new Date(t).getTime(),i=this.currentTimestampMs-r,o=this.config.noDeleteBeforeSeconds*1e3;return i>o?{violation:`Cannot delete object: object is ${Math.floor(i/1e3)} seconds old, which exceeds the ${this.config.noDeleteBeforeSeconds} seconds threshold`}:null}catch(e){if(e instanceof u&&e.code===`404`)return null;throw e}}};const c=e=>n.array(n.object({pattern:n.string(),config:e.nullable()})),l=n.object({noDeleteOld:c(o)});var u=class extends Error{code;constructor(e,t){super(t),this.code=e}};async function d(e,t,n,r,i){let a=new URL(e.url).pathname,o=f(i,a,t,n,r);if(o.length===0)return null;let s=o.map(({name:t,policy:n})=>({policyName:t,promise:(async()=>{let r=await n.evaluate(e);return r?{...r,policy:t}:null})()}));return new Promise(e=>{let t=s.length;for(let{policyName:n,promise:r}of s)r.then(n=>{n===null?(t--,t===0&&e(null)):e(n)}).catch(t=>{let r=t instanceof Error?t.message:String(t);t instanceof u&&(r=`Error calling upstream when evaluating guardrail: ${r}`),e({violation:r,policy:n})})})}function f(e,t,n,r,i){let a=[];for(let o of e.noDeleteOld){let e=new RegExp(o.pattern);if(e.test(t)){o.config!==null&&a.push({name:`noDeleteOld`,policy:new s(o.config,n,r,i)});break}}return a}const p=new Set(`x-amz-date.x-amz-content-sha256.x-amz-security-token.x-amz-server-side-encryption.x-amz-server-side-encryption-aws-kms-key-id.x-amz-server-side-encryption-customer-algorithm.x-amz-server-side-encryption-customer-key.x-amz-server-side-encryption-customer-key-md5.x-amz-storage-class.x-amz-tagging.x-amz-website-redirect-location.x-amz-acl.x-amz-grant-read.x-amz-grant-write.x-amz-grant-read-acp.x-amz-grant-write-acp.x-amz-grant-full-control.x-amz-metadata-directive.x-amz-copy-source.x-amz-copy-source-if-match.x-amz-copy-source-if-none-match.x-amz-copy-source-if-unmodified-since.x-amz-copy-source-if-modified-since.x-amz-copy-source-range.content-type.content-length.content-md5.content-encoding.content-disposition.cache-control.expires.range.if-match.if-none-match.if-modified-since.if-unmodified-since.user-agent`.split(`.`)),m=new Set([`X-Amz-Algorithm`,`X-Amz-Credential`,`X-Amz-Date`,`X-Amz-Expires`,`X-Amz-SignedHeaders`,`X-Amz-Signature`,`X-Amz-Security-Token`]),h={noDeleteOld:[{pattern:`/.*`,config:{noDeleteBeforeSeconds:60}}]};async function g(n,a){let o=Date.now(),s=await e(n,a.clientSecretAccessKey,a.clientAccessKeyId,o);if(!s.valid)return i(`Signature verification failed: ${s.error}`,r.Forbidden);let c=new URL(n.url),l=new t({accessKeyId:a.upstreamAccessKeyId,secretAccessKey:a.upstreamSecretAccessKey,retries:5}),u=await d(n,l,a.s3Endpoint,o,a.guardrailConfig||h);if(u)return console.log(`Guardrail violation`,{path:c.pathname,method:n.method,query:c.search,policy:u.policy,violation:u.violation}),i(`Request violating guardrail policy ${u.policy}: ${u.violation}`,r.Forbidden);let f=new URL(c.pathname,a.s3Endpoint);for(let[e,t]of c.searchParams.entries())m.has(e)||f.searchParams.set(e,t);let g=new Headers;for(let[e,t]of n.headers.entries())p.has(e.toLowerCase())&&g.set(e,t);g.set(`x-amz-content-sha256`,`UNSIGNED-PAYLOAD`);let _=new Request(f.toString(),{method:n.method,headers:g,body:n.body,duplex:`half`}),v=new t({accessKeyId:a.upstreamAccessKeyId,secretAccessKey:a.upstreamSecretAccessKey,retries:0});try{return await v.fetch(_)}catch(e){return console.error(`Upstream request failed:`,e),i(`Upstream request failed: ${e instanceof Error?e.message:`Unknown error`}`,r.UpstreamFailure)}}export{l as GuardrailConfigZod,h as defaultGuardrailConfig,g as handle};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["text: string","errorCode: ErrorCode","upstream: AwsClient","objectPath: string","config: NoDeleteOldPolicyConfig","upstreamFetcher: AwsClient","upstreamEndpoint: string","currentTimestampMs: number","request: Request<unknown, IncomingRequestCfProperties>","policy: T","code: string","message: string","request: Request<unknown, IncomingRequestCfProperties>","upstreamFetcher: AwsClient","s3_endpoint: string","currentTimestampMs: number","config: GuardrailConfig","path: string","upstreamEndpoint: string","policies: { name: string; policy: GuardrailPolicy }[]","defaultGuardrailConfig: GuardrailConfig","request: Request<unknown, IncomingRequestCfProperties>","options: S3BrokerOptions"],"sources":["../src/utils.ts","../src/guardrails/s3_helper.ts","../src/guardrails/no-delete-old.ts","../src/guardrails/type.ts","../src/guardrails/guardrails.ts","../src/index.ts"],"sourcesContent":["export enum ErrorCode {\n\tForbidden = 403,\n\tUpstreamFailure = 502,\n}\n\nexport function textErrorResponse(text: string, errorCode: ErrorCode): Response {\n\treturn new Response(text, { status: errorCode, headers: { 'Content-Type': 'text/plain' } });\n}\n","import { AwsClient } from 'aws4fetch';\nimport { UpstreamError } from './type';\n/**\n * Issue a `HEAD` request to retrieve object metadata\n * @param upstream\n * @param objectPath\n */\nexport async function getObjectMetadata(upstream: AwsClient, objectPath: string): Promise<Headers> {\n\tconst response = await upstream.fetch(objectPath, {\n\t\tmethod: 'HEAD',\n\t});\n\tif (!response.ok) {\n\t\tthrow new UpstreamError(response.status.toString(), response.statusText);\n\t}\n\treturn response.headers;\n}\n","import { GuardrailPolicy, GuardrailViolation, UpstreamError } from './type';\nimport { z } from 'zod';\nimport { AwsClient } from 'aws4fetch';\nimport { getObjectMetadata } from './s3_helper';\n\nexport const NoDeleteOldPolicyConfig = z.object({\n\tnoDeleteBeforeSeconds: z.number().int(),\n});\n\nexport type NoDeleteOldPolicyConfig = z.infer<typeof NoDeleteOldPolicyConfig>;\n\nexport class NoDeleteOldPolicy implements GuardrailPolicy {\n\tprivate config: NoDeleteOldPolicyConfig;\n\tprivate upstreamFetcher: AwsClient;\n\tprivate upstreamEndpoint: string;\n\tprivate currentTimestampMs: number;\n\n\tconstructor(config: NoDeleteOldPolicyConfig, upstreamFetcher: AwsClient, upstreamEndpoint: string, currentTimestampMs: number) {\n\t\tthis.config = config;\n\t\tthis.upstreamFetcher = upstreamFetcher;\n\t\tthis.upstreamEndpoint = upstreamEndpoint;\n\t\tthis.currentTimestampMs = currentTimestampMs;\n\t}\n\n\tasync evaluate(request: Request<unknown, IncomingRequestCfProperties>): Promise<GuardrailViolation | null> {\n\t\t// Only applies to DELETE requests\n\t\tif (request.method !== 'DELETE') {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst url = new URL(request.url);\n\t\tconst objectPath = this.upstreamEndpoint + url.pathname;\n\n\t\ttry {\n\t\t\tconst headers = await getObjectMetadata(this.upstreamFetcher, objectPath);\n\n\t\t\t// Get the Last-Modified header to determine object age\n\t\t\tconst lastModified = headers.get('Last-Modified');\n\t\t\tif (!lastModified) {\n\t\t\t\t// If no Last-Modified header, allow deletion (object might be newly created)\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst objectCreatedAtMs = new Date(lastModified).getTime();\n\t\t\tconst objectAgeMs = this.currentTimestampMs - objectCreatedAtMs;\n\t\t\tconst thresholdMs = this.config.noDeleteBeforeSeconds * 1000;\n\n\t\t\tif (objectAgeMs > thresholdMs) {\n\t\t\t\treturn {\n\t\t\t\t\tviolation: `Cannot delete object: object is ${Math.floor(objectAgeMs / 1000)} seconds old, which exceeds the ${this.config.noDeleteBeforeSeconds} seconds threshold`,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn null;\n\t\t} catch (error) {\n\t\t\tif (error instanceof UpstreamError && error.code === '404') {\n\t\t\t\t// Object doesn't exist, allow deletion attempt (will fail at upstream)\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n","import { z, ZodType } from 'zod';\nimport { NoDeleteOldPolicyConfig } from './no-delete-old';\n\nexport interface GuardrailPolicy {\n\tevaluate(request: Request<unknown, IncomingRequestCfProperties>): Promise<GuardrailViolation | null>;\n}\n\nexport type GuardrailViolation = {\n\tviolation: string;\n};\n\n/*\n * Corresponding config for each object path pattern in regex. First match wins.\n */\nexport const GuardrailPolicyConfigPerPattern = <T extends ZodType>(policy: T) =>\n\tz.array(\n\t\tz.object({\n\t\t\tpattern: z.string(),\n\t\t\tconfig: policy,\n\t\t}),\n\t);\nexport const GuardrailConfig = z.object({\n\tnoDeleteOld: GuardrailPolicyConfigPerPattern(NoDeleteOldPolicyConfig),\n});\nexport type GuardrailConfig = z.infer<typeof GuardrailConfig>;\n\n/**\n * Error calling upstream when evaluating guardrails\n */\nexport class UpstreamError extends Error {\n\tcode: string;\n\tconstructor(code: string, message: string) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t}\n}\n","import { AwsClient } from 'aws4fetch';\nimport { GuardrailConfig, GuardrailPolicy, GuardrailViolation, UpstreamError } from './type';\nimport { NoDeleteOldPolicy } from './no-delete-old';\n\n/**\n * Evaluate guardrails for a given request\n * @param request The incoming request\n * @param config The guardrail configuration to use\n * @returns The first guardrail violation if the request violates a policy, null otherwise\n */\nexport async function evaluateGuardrails(\n\trequest: Request<unknown, IncomingRequestCfProperties>,\n\tupstreamFetcher: AwsClient,\n\ts3_endpoint: string,\n\tcurrentTimestampMs: number,\n\tconfig: GuardrailConfig,\n): Promise<(GuardrailViolation & { policy: string }) | null> {\n\tconst path = new URL(request.url).pathname;\n\tconst policies = getPolicies(config, path, upstreamFetcher, s3_endpoint, currentTimestampMs);\n\n\tif (policies.length === 0) {\n\t\treturn null;\n\t}\n\n\t// Create evaluation promises that include policy name\n\tconst evalPromises = policies.map(({ name: policyName, policy }) => ({\n\t\tpolicyName,\n\t\tpromise: (async () => {\n\t\t\tconst violation = await policy.evaluate(request);\n\t\t\treturn violation ? { ...violation, policy: policyName } : null;\n\t\t})(),\n\t}));\n\n\t// Race until one returns a violation or all are done\n\t// Using Promise.race with a filter pattern to get first non-null result\n\treturn new Promise((resolve) => {\n\t\tlet pendingCount = evalPromises.length;\n\n\t\tfor (const { policyName, promise } of evalPromises) {\n\t\t\tpromise\n\t\t\t\t.then((result) => {\n\t\t\t\t\tif (result !== null) {\n\t\t\t\t\t\t// First violation wins\n\t\t\t\t\t\tresolve(result);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpendingCount--;\n\t\t\t\t\t\tif (pendingCount === 0) {\n\t\t\t\t\t\t\t// All policies passed\n\t\t\t\t\t\t\tresolve(null);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\t// If a policy throws, treat it as a violation\n\t\t\t\t\tlet message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tif (error instanceof UpstreamError) {\n\t\t\t\t\t\tmessage = `Error calling upstream when evaluating guardrail: ${message}`;\n\t\t\t\t\t}\n\t\t\t\t\tresolve({ violation: message, policy: policyName });\n\t\t\t\t});\n\t\t}\n\t});\n}\n\n/**\n * Get all applicable policies for a given path\n */\nexport function getPolicies(\n\tconfig: GuardrailConfig,\n\tpath: string,\n\tupstreamFetcher: AwsClient,\n\tupstreamEndpoint: string,\n\tcurrentTimestampMs: number,\n): { name: string; policy: GuardrailPolicy }[] {\n\tconst policies: { name: string; policy: GuardrailPolicy }[] = [];\n\n\t// Check noDeleteOld policies - first matching pattern wins\n\tfor (const entry of config.noDeleteOld) {\n\t\tconst regex = new RegExp(entry.pattern);\n\t\tif (regex.test(path)) {\n\t\t\tpolicies.push({\n\t\t\t\tname: 'noDeleteOld',\n\t\t\t\tpolicy: new NoDeleteOldPolicy(entry.config, upstreamFetcher, upstreamEndpoint, currentTimestampMs),\n\t\t\t});\n\t\t\tbreak; // First match wins\n\t\t}\n\t}\n\n\treturn policies;\n}\n","/**\n * S3Broker - S3 Proxy Library for Cloudflare Workers\n *\n * ========== =========== ============\n * ||Client|| -- Key A --> ||S3Broker|| -- Key B --> ||Upstream||\n * ========== =========== ============\n *\n * S3Broker is a Cloudflare Workers library for building secure S3-compatible proxies.\n *\n * Features:\n * 1. Verifies incoming requests signed with Key A (client credentials)\n * 2. Enforces configurable guardrails policies (e.g., prevent deletion of recent objects)\n * 3. Re-signs requests with Key B (upstream credentials) for the upstream S3 service\n * 4. Proxies the request to any S3-compatible endpoint (AWS S3, Cloudflare R2, MinIO, etc.)\n */\n\nimport { AwsClient } from 'aws4fetch';\nimport { verifySignature } from './sigv4';\nimport { textErrorResponse, ErrorCode } from './utils';\nimport { evaluateGuardrails } from './guardrails/guardrails';\nimport type { S3BrokerOptions } from './types';\nimport { GuardrailConfig } from './guardrails/type';\n\n// Re-export types\nexport type { S3BrokerOptions } from './types';\nexport type { GuardrailConfig, GuardrailViolation } from './guardrails/type';\n\n// Headers that should be forwarded to upstream (allowlist approach)\nconst HEADERS_TO_INCLUDE = new Set([\n\t// S3-specific headers\n\t'x-amz-date',\n\t'x-amz-content-sha256',\n\t'x-amz-security-token',\n\t'x-amz-server-side-encryption',\n\t'x-amz-server-side-encryption-aws-kms-key-id',\n\t'x-amz-server-side-encryption-customer-algorithm',\n\t'x-amz-server-side-encryption-customer-key',\n\t'x-amz-server-side-encryption-customer-key-md5',\n\t'x-amz-storage-class',\n\t'x-amz-tagging',\n\t'x-amz-website-redirect-location',\n\t'x-amz-acl',\n\t'x-amz-grant-read',\n\t'x-amz-grant-write',\n\t'x-amz-grant-read-acp',\n\t'x-amz-grant-write-acp',\n\t'x-amz-grant-full-control',\n\t'x-amz-metadata-directive',\n\t'x-amz-copy-source',\n\t'x-amz-copy-source-if-match',\n\t'x-amz-copy-source-if-none-match',\n\t'x-amz-copy-source-if-unmodified-since',\n\t'x-amz-copy-source-if-modified-since',\n\t'x-amz-copy-source-range',\n\t// Standard HTTP headers that S3 uses\n\t'content-type',\n\t'content-length',\n\t'content-md5',\n\t'content-encoding',\n\t'content-disposition',\n\t'cache-control',\n\t'expires',\n\t'range',\n\t'if-match',\n\t'if-none-match',\n\t'if-modified-since',\n\t'if-unmodified-since',\n\t'user-agent',\n]);\n\n// Presigned URL parameters that should be stripped when re-signing\nconst PRESIGNED_PARAMS = new Set([\n\t'X-Amz-Algorithm',\n\t'X-Amz-Credential',\n\t'X-Amz-Date',\n\t'X-Amz-Expires',\n\t'X-Amz-SignedHeaders',\n\t'X-Amz-Signature',\n\t'X-Amz-Security-Token',\n]);\n\nexport const defaultGuardrailConfig: GuardrailConfig = {\n\tnoDeleteOld: [\n\t\t{\n\t\t\tpattern: '/.*',\n\t\t\tconfig: {\n\t\t\t\tnoDeleteBeforeSeconds: 60,\n\t\t\t},\n\t\t},\n\t],\n};\n\n/**\n * Handle an incoming S3 request with signature verification, guardrails, and proxying.\n *\n * @param request - The incoming HTTP request (must be a valid S3 API request)\n * @param options - S3Broker configuration options including credentials and guardrails\n * @returns Response from the upstream S3 service, or an error response if validation fails\n *\n * @example\n * ```typescript\n * import { handle } from 's3broker';\n *\n * const response = await handle(request, ctx, {\n * s3Endpoint: 'https://my-bucket.s3.amazonaws.com',\n * clientAccessKeyId: 'CLIENT_KEY',\n * clientSecretAccessKey: 'CLIENT_SECRET',\n * upstreamAccessKeyId: 'UPSTREAM_KEY',\n * upstreamSecretAccessKey: 'UPSTREAM_SECRET',\n * });\n * ```\n */\nexport async function handle(request: Request<unknown, IncomingRequestCfProperties>, options: S3BrokerOptions): Promise<Response> {\n\tconst currentTimestamp = Date.now();\n\n\t// Verify the incoming request signature (Client Key)\n\tconst verificationResult = await verifySignature(request, options.clientSecretAccessKey, options.clientAccessKeyId, currentTimestamp);\n\n\tif (!verificationResult.valid) {\n\t\treturn textErrorResponse(`Signature verification failed: ${verificationResult.error}`, ErrorCode.Forbidden);\n\t}\n\n\t// Parse the request URL\n\tconst url = new URL(request.url);\n\n\t// Evaluate guardrails\n\tconst guardrailUpstreamClient = new AwsClient({\n\t\taccessKeyId: options.upstreamAccessKeyId,\n\t\tsecretAccessKey: options.upstreamSecretAccessKey,\n\t\tretries: 5,\n\t});\n\n\tconst guardrailViolation = await evaluateGuardrails(\n\t\trequest,\n\t\tguardrailUpstreamClient,\n\t\toptions.s3Endpoint,\n\t\tcurrentTimestamp,\n\t\toptions.guardrailConfig || defaultGuardrailConfig,\n\t);\n\n\tif (guardrailViolation) {\n\t\tconsole.log(`Guardrail violation`, {\n\t\t\tpath: url.pathname,\n\t\t\tmethod: request.method,\n\t\t\tquery: url.search,\n\t\t\tpolicy: guardrailViolation.policy,\n\t\t\tviolation: guardrailViolation.violation,\n\t\t});\n\t\treturn textErrorResponse(\n\t\t\t`Request violating guardrail policy ${guardrailViolation.policy}: ${guardrailViolation.violation}`,\n\t\t\tErrorCode.Forbidden,\n\t\t);\n\t}\n\n\t// Build upstream URL, stripping presigned parameters\n\tconst upstreamUrl = new URL(url.pathname, options.s3Endpoint);\n\tfor (const [key, value] of url.searchParams.entries()) {\n\t\tif (!PRESIGNED_PARAMS.has(key)) {\n\t\t\tupstreamUrl.searchParams.set(key, value);\n\t\t}\n\t}\n\n\t// Build upstream headers (allowlist approach)\n\tconst upstreamHeaders = new Headers();\n\tfor (const [key, value] of request.headers.entries()) {\n\t\tif (HEADERS_TO_INCLUDE.has(key.toLowerCase())) {\n\t\t\tupstreamHeaders.set(key, value);\n\t\t}\n\t}\n\tupstreamHeaders.set('x-amz-content-sha256', 'UNSIGNED-PAYLOAD');\n\n\t// Create upstream request\n\tconst upstreamRequest = new Request(upstreamUrl.toString(), {\n\t\tmethod: request.method,\n\t\theaders: upstreamHeaders,\n\t\tbody: request.body,\n\t\t// @ts-ignore - duplex is needed for streaming bodies\n\t\tduplex: 'half',\n\t});\n\n\t// Sign and send to upstream\n\tconst proxyUpstreamAws = new AwsClient({\n\t\taccessKeyId: options.upstreamAccessKeyId,\n\t\tsecretAccessKey: options.upstreamSecretAccessKey,\n\t\tretries: 0,\n\t});\n\n\ttry {\n\t\treturn await proxyUpstreamAws.fetch(upstreamRequest);\n\t} catch (error) {\n\t\tconsole.error('Upstream request failed:', error);\n\t\treturn textErrorResponse(\n\t\t\t`Upstream request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,\n\t\t\tErrorCode.UpstreamFailure,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["text: string","errorCode: ErrorCode","upstream: AwsClient","objectPath: string","config: NoDeleteOldPolicyConfig","upstreamFetcher: AwsClient","upstreamEndpoint: string","currentTimestampMs: number","request: Request<unknown, IncomingRequestCfProperties>","policy: T","code: string","message: string","request: Request<unknown, IncomingRequestCfProperties>","upstreamFetcher: AwsClient","s3_endpoint: string","currentTimestampMs: number","config: GuardrailConfig","path: string","upstreamEndpoint: string","policies: { name: string; policy: GuardrailPolicy }[]","defaultGuardrailConfig: GuardrailConfig","request: Request<unknown, IncomingRequestCfProperties>","options: S3BrokerOptions"],"sources":["../src/utils.ts","../src/guardrails/s3_helper.ts","../src/guardrails/no-delete-old.ts","../src/guardrails/type.ts","../src/guardrails/guardrails.ts","../src/index.ts"],"sourcesContent":["export enum ErrorCode {\n\tForbidden = 403,\n\tUpstreamFailure = 502,\n}\n\nexport function textErrorResponse(text: string, errorCode: ErrorCode): Response {\n\treturn new Response(text, { status: errorCode, headers: { 'Content-Type': 'text/plain' } });\n}\n","import { AwsClient } from 'aws4fetch';\nimport { UpstreamError } from './type';\n/**\n * Issue a `HEAD` request to retrieve object metadata\n * @param upstream\n * @param objectPath\n */\nexport async function getObjectMetadata(upstream: AwsClient, objectPath: string): Promise<Headers> {\n\tconst response = await upstream.fetch(objectPath, {\n\t\tmethod: 'HEAD',\n\t});\n\tif (!response.ok) {\n\t\tthrow new UpstreamError(response.status.toString(), response.statusText);\n\t}\n\treturn response.headers;\n}\n","import { GuardrailPolicy, GuardrailViolation, UpstreamError } from './type';\nimport { z } from 'zod';\nimport { AwsClient } from 'aws4fetch';\nimport { getObjectMetadata } from './s3_helper';\n\nexport const NoDeleteOldPolicyConfig = z.object({\n\tnoDeleteBeforeSeconds: z.number().int(),\n});\n\nexport type NoDeleteOldPolicyConfig = z.infer<typeof NoDeleteOldPolicyConfig>;\n\nexport class NoDeleteOldPolicy implements GuardrailPolicy {\n\tprivate config: NoDeleteOldPolicyConfig;\n\tprivate upstreamFetcher: AwsClient;\n\tprivate upstreamEndpoint: string;\n\tprivate currentTimestampMs: number;\n\n\tconstructor(config: NoDeleteOldPolicyConfig, upstreamFetcher: AwsClient, upstreamEndpoint: string, currentTimestampMs: number) {\n\t\tthis.config = config;\n\t\tthis.upstreamFetcher = upstreamFetcher;\n\t\tthis.upstreamEndpoint = upstreamEndpoint;\n\t\tthis.currentTimestampMs = currentTimestampMs;\n\t}\n\n\tasync evaluate(request: Request<unknown, IncomingRequestCfProperties>): Promise<GuardrailViolation | null> {\n\t\t// Only applies to DELETE requests\n\t\tif (request.method !== 'DELETE') {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst url = new URL(request.url);\n\t\tconst objectPath = this.upstreamEndpoint + url.pathname;\n\n\t\ttry {\n\t\t\tconst headers = await getObjectMetadata(this.upstreamFetcher, objectPath);\n\n\t\t\t// Get the Last-Modified header to determine object age\n\t\t\tconst lastModified = headers.get('Last-Modified');\n\t\t\tif (!lastModified) {\n\t\t\t\t// If no Last-Modified header, allow deletion (object might be newly created)\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst objectCreatedAtMs = new Date(lastModified).getTime();\n\t\t\tconst objectAgeMs = this.currentTimestampMs - objectCreatedAtMs;\n\t\t\tconst thresholdMs = this.config.noDeleteBeforeSeconds * 1000;\n\n\t\t\tif (objectAgeMs > thresholdMs) {\n\t\t\t\treturn {\n\t\t\t\t\tviolation: `Cannot delete object: object is ${Math.floor(objectAgeMs / 1000)} seconds old, which exceeds the ${this.config.noDeleteBeforeSeconds} seconds threshold`,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn null;\n\t\t} catch (error) {\n\t\t\tif (error instanceof UpstreamError && error.code === '404') {\n\t\t\t\t// Object doesn't exist, allow deletion attempt (will fail at upstream)\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n","import { z, ZodType } from 'zod';\nimport { NoDeleteOldPolicyConfig } from './no-delete-old';\n\nexport interface GuardrailPolicy {\n\tevaluate(request: Request<unknown, IncomingRequestCfProperties>): Promise<GuardrailViolation | null>;\n}\n\nexport type GuardrailViolation = {\n\tviolation: string;\n};\n\n/*\n * Corresponding config for each object path pattern in regex. First match wins.\n * If config is null, the guardrail is disabled for that pattern.\n */\nexport const GuardrailPolicyConfigPerPattern = <T extends ZodType>(policy: T) =>\n\tz.array(\n\t\tz.object({\n\t\t\tpattern: z.string(),\n\t\t\tconfig: policy.nullable(),\n\t\t}),\n\t);\nexport const GuardrailConfig = z.object({\n\tnoDeleteOld: GuardrailPolicyConfigPerPattern(NoDeleteOldPolicyConfig),\n});\nexport type GuardrailConfig = z.infer<typeof GuardrailConfig>;\n\n/**\n * Error calling upstream when evaluating guardrails\n */\nexport class UpstreamError extends Error {\n\tcode: string;\n\tconstructor(code: string, message: string) {\n\t\tsuper(message);\n\t\tthis.code = code;\n\t}\n}\n","import { AwsClient } from 'aws4fetch';\nimport { GuardrailConfig, GuardrailPolicy, GuardrailViolation, UpstreamError } from './type';\nimport { NoDeleteOldPolicy } from './no-delete-old';\n\n/**\n * Evaluate guardrails for a given request\n * @param request The incoming request\n * @param config The guardrail configuration to use\n * @returns The first guardrail violation if the request violates a policy, null otherwise\n */\nexport async function evaluateGuardrails(\n\trequest: Request<unknown, IncomingRequestCfProperties>,\n\tupstreamFetcher: AwsClient,\n\ts3_endpoint: string,\n\tcurrentTimestampMs: number,\n\tconfig: GuardrailConfig,\n): Promise<(GuardrailViolation & { policy: string }) | null> {\n\tconst path = new URL(request.url).pathname;\n\tconst policies = getPolicies(config, path, upstreamFetcher, s3_endpoint, currentTimestampMs);\n\n\tif (policies.length === 0) {\n\t\treturn null;\n\t}\n\n\t// Create evaluation promises that include policy name\n\tconst evalPromises = policies.map(({ name: policyName, policy }) => ({\n\t\tpolicyName,\n\t\tpromise: (async () => {\n\t\t\tconst violation = await policy.evaluate(request);\n\t\t\treturn violation ? { ...violation, policy: policyName } : null;\n\t\t})(),\n\t}));\n\n\t// Race until one returns a violation or all are done\n\t// Using Promise.race with a filter pattern to get first non-null result\n\treturn new Promise((resolve) => {\n\t\tlet pendingCount = evalPromises.length;\n\n\t\tfor (const { policyName, promise } of evalPromises) {\n\t\t\tpromise\n\t\t\t\t.then((result) => {\n\t\t\t\t\tif (result !== null) {\n\t\t\t\t\t\t// First violation wins\n\t\t\t\t\t\tresolve(result);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpendingCount--;\n\t\t\t\t\t\tif (pendingCount === 0) {\n\t\t\t\t\t\t\t// All policies passed\n\t\t\t\t\t\t\tresolve(null);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\t// If a policy throws, treat it as a violation\n\t\t\t\t\tlet message = error instanceof Error ? error.message : String(error);\n\t\t\t\t\tif (error instanceof UpstreamError) {\n\t\t\t\t\t\tmessage = `Error calling upstream when evaluating guardrail: ${message}`;\n\t\t\t\t\t}\n\t\t\t\t\tresolve({ violation: message, policy: policyName });\n\t\t\t\t});\n\t\t}\n\t});\n}\n\n/**\n * Get all applicable policies for a given path\n */\nexport function getPolicies(\n\tconfig: GuardrailConfig,\n\tpath: string,\n\tupstreamFetcher: AwsClient,\n\tupstreamEndpoint: string,\n\tcurrentTimestampMs: number,\n): { name: string; policy: GuardrailPolicy }[] {\n\tconst policies: { name: string; policy: GuardrailPolicy }[] = [];\n\n\t// Check noDeleteOld policies - first matching pattern wins\n\t// If config is null, the guardrail is disabled for that pattern (exclude case)\n\tfor (const entry of config.noDeleteOld) {\n\t\tconst regex = new RegExp(entry.pattern);\n\t\tif (regex.test(path)) {\n\t\t\tif (entry.config !== null) {\n\t\t\t\tpolicies.push({\n\t\t\t\t\tname: 'noDeleteOld',\n\t\t\t\t\tpolicy: new NoDeleteOldPolicy(entry.config, upstreamFetcher, upstreamEndpoint, currentTimestampMs),\n\t\t\t\t});\n\t\t\t}\n\t\t\tbreak; // First match wins - even if config is null, we don't check further patterns\n\t\t}\n\t}\n\n\treturn policies;\n}\n","/**\n * S3Broker - S3 Proxy Library for Cloudflare Workers\n *\n * ========== =========== ============\n * ||Client|| -- Key A --> ||S3Broker|| -- Key B --> ||Upstream||\n * ========== =========== ============\n *\n * S3Broker is a Cloudflare Workers library for building secure S3-compatible proxies.\n *\n * Features:\n * 1. Verifies incoming requests signed with Key A (client credentials)\n * 2. Enforces configurable guardrails policies (e.g., prevent deletion of recent objects)\n * 3. Re-signs requests with Key B (upstream credentials) for the upstream S3 service\n * 4. Proxies the request to any S3-compatible endpoint (AWS S3, Cloudflare R2, MinIO, etc.)\n */\n\nimport { AwsClient } from 'aws4fetch';\nimport { verifySignature } from './sigv4';\nimport { textErrorResponse, ErrorCode } from './utils';\nimport { evaluateGuardrails } from './guardrails/guardrails';\nimport type { S3BrokerOptions } from './types';\nimport { GuardrailConfig } from './guardrails/type';\n\n// Re-export types\nexport type { S3BrokerOptions } from './types';\nexport type { GuardrailConfig, GuardrailViolation } from './guardrails/type';\n// Re-export zod type\nexport { GuardrailConfig as GuardrailConfigZod } from './guardrails/type';\n\n// Headers that should be forwarded to upstream (allowlist approach)\nconst HEADERS_TO_INCLUDE = new Set([\n\t// S3-specific headers\n\t'x-amz-date',\n\t'x-amz-content-sha256',\n\t'x-amz-security-token',\n\t'x-amz-server-side-encryption',\n\t'x-amz-server-side-encryption-aws-kms-key-id',\n\t'x-amz-server-side-encryption-customer-algorithm',\n\t'x-amz-server-side-encryption-customer-key',\n\t'x-amz-server-side-encryption-customer-key-md5',\n\t'x-amz-storage-class',\n\t'x-amz-tagging',\n\t'x-amz-website-redirect-location',\n\t'x-amz-acl',\n\t'x-amz-grant-read',\n\t'x-amz-grant-write',\n\t'x-amz-grant-read-acp',\n\t'x-amz-grant-write-acp',\n\t'x-amz-grant-full-control',\n\t'x-amz-metadata-directive',\n\t'x-amz-copy-source',\n\t'x-amz-copy-source-if-match',\n\t'x-amz-copy-source-if-none-match',\n\t'x-amz-copy-source-if-unmodified-since',\n\t'x-amz-copy-source-if-modified-since',\n\t'x-amz-copy-source-range',\n\t// Standard HTTP headers that S3 uses\n\t'content-type',\n\t'content-length',\n\t'content-md5',\n\t'content-encoding',\n\t'content-disposition',\n\t'cache-control',\n\t'expires',\n\t'range',\n\t'if-match',\n\t'if-none-match',\n\t'if-modified-since',\n\t'if-unmodified-since',\n\t'user-agent',\n]);\n\n// Presigned URL parameters that should be stripped when re-signing\nconst PRESIGNED_PARAMS = new Set([\n\t'X-Amz-Algorithm',\n\t'X-Amz-Credential',\n\t'X-Amz-Date',\n\t'X-Amz-Expires',\n\t'X-Amz-SignedHeaders',\n\t'X-Amz-Signature',\n\t'X-Amz-Security-Token',\n]);\n\nexport const defaultGuardrailConfig: GuardrailConfig = {\n\tnoDeleteOld: [\n\t\t{\n\t\t\tpattern: '/.*',\n\t\t\tconfig: {\n\t\t\t\tnoDeleteBeforeSeconds: 60,\n\t\t\t},\n\t\t},\n\t],\n};\n\n/**\n * Handle an incoming S3 request with signature verification, guardrails, and proxying.\n *\n * @param request - The incoming HTTP request (must be a valid S3 API request)\n * @param options - S3Broker configuration options including credentials and guardrails\n * @returns Response from the upstream S3 service, or an error response if validation fails\n *\n * @example\n * ```typescript\n * import { handle } from 's3broker';\n *\n * const response = await handle(request, ctx, {\n * s3Endpoint: 'https://my-bucket.s3.amazonaws.com',\n * clientAccessKeyId: 'CLIENT_KEY',\n * clientSecretAccessKey: 'CLIENT_SECRET',\n * upstreamAccessKeyId: 'UPSTREAM_KEY',\n * upstreamSecretAccessKey: 'UPSTREAM_SECRET',\n * });\n * ```\n */\nexport async function handle(request: Request<unknown, IncomingRequestCfProperties>, options: S3BrokerOptions): Promise<Response> {\n\tconst currentTimestamp = Date.now();\n\n\t// Verify the incoming request signature (Client Key)\n\tconst verificationResult = await verifySignature(request, options.clientSecretAccessKey, options.clientAccessKeyId, currentTimestamp);\n\n\tif (!verificationResult.valid) {\n\t\treturn textErrorResponse(`Signature verification failed: ${verificationResult.error}`, ErrorCode.Forbidden);\n\t}\n\n\t// Parse the request URL\n\tconst url = new URL(request.url);\n\n\t// Evaluate guardrails\n\tconst guardrailUpstreamClient = new AwsClient({\n\t\taccessKeyId: options.upstreamAccessKeyId,\n\t\tsecretAccessKey: options.upstreamSecretAccessKey,\n\t\tretries: 5,\n\t});\n\n\tconst guardrailViolation = await evaluateGuardrails(\n\t\trequest,\n\t\tguardrailUpstreamClient,\n\t\toptions.s3Endpoint,\n\t\tcurrentTimestamp,\n\t\toptions.guardrailConfig || defaultGuardrailConfig,\n\t);\n\n\tif (guardrailViolation) {\n\t\tconsole.log(`Guardrail violation`, {\n\t\t\tpath: url.pathname,\n\t\t\tmethod: request.method,\n\t\t\tquery: url.search,\n\t\t\tpolicy: guardrailViolation.policy,\n\t\t\tviolation: guardrailViolation.violation,\n\t\t});\n\t\treturn textErrorResponse(\n\t\t\t`Request violating guardrail policy ${guardrailViolation.policy}: ${guardrailViolation.violation}`,\n\t\t\tErrorCode.Forbidden,\n\t\t);\n\t}\n\n\t// Build upstream URL, stripping presigned parameters\n\tconst upstreamUrl = new URL(url.pathname, options.s3Endpoint);\n\tfor (const [key, value] of url.searchParams.entries()) {\n\t\tif (!PRESIGNED_PARAMS.has(key)) {\n\t\t\tupstreamUrl.searchParams.set(key, value);\n\t\t}\n\t}\n\n\t// Build upstream headers (allowlist approach)\n\tconst upstreamHeaders = new Headers();\n\tfor (const [key, value] of request.headers.entries()) {\n\t\tif (HEADERS_TO_INCLUDE.has(key.toLowerCase())) {\n\t\t\tupstreamHeaders.set(key, value);\n\t\t}\n\t}\n\tupstreamHeaders.set('x-amz-content-sha256', 'UNSIGNED-PAYLOAD');\n\n\t// Create upstream request\n\tconst upstreamRequest = new Request(upstreamUrl.toString(), {\n\t\tmethod: request.method,\n\t\theaders: upstreamHeaders,\n\t\tbody: request.body,\n\t\t// @ts-ignore - duplex is needed for streaming bodies\n\t\tduplex: 'half',\n\t});\n\n\t// Sign and send to upstream\n\tconst proxyUpstreamAws = new AwsClient({\n\t\taccessKeyId: options.upstreamAccessKeyId,\n\t\tsecretAccessKey: options.upstreamSecretAccessKey,\n\t\tretries: 0,\n\t});\n\n\ttry {\n\t\treturn await proxyUpstreamAws.fetch(upstreamRequest);\n\t} catch (error) {\n\t\tconsole.error('Upstream request failed:', error);\n\t\treturn textErrorResponse(\n\t\t\t`Upstream request failed: ${error instanceof Error ? error.message : 'Unknown error'}`,\n\t\t\tErrorCode.UpstreamFailure,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
|