s3broker 0.4.3 → 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 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: '/protected/.*',
68
- config: { noDeleteBeforeSeconds: 3600 }, // Files older than 1h in /protected/ could not be deleted
69
+ pattern: '/frequent_updated/.*',
70
+ config: null,
71
+ },
72
+ {
73
+ pattern: '/.*',
74
+ config: { noDeleteBeforeSeconds: 3600 },
69
75
  },
70
76
  ],
71
77
  },
@@ -0,0 +1,93 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/guardrails/type.d.ts
4
+
5
+ type GuardrailViolation = {
6
+ violation: string;
7
+ };
8
+ declare const GuardrailConfig: z.ZodObject<{
9
+ noDeleteOld: z.ZodArray<z.ZodObject<{
10
+ pattern: z.ZodString;
11
+ config: z.ZodNullable<z.ZodObject<{
12
+ noDeleteBeforeSeconds: z.ZodNumber;
13
+ }, z.core.$strip>>;
14
+ }, z.core.$strip>>;
15
+ }, z.core.$strip>;
16
+ type GuardrailConfig = z.infer<typeof GuardrailConfig>;
17
+ /**
18
+ * Error calling upstream when evaluating guardrails
19
+ */
20
+ //#endregion
21
+ //#region src/types.d.ts
22
+ /**
23
+ * Configuration options for the S3Broker handler.
24
+ *
25
+ * S3Broker uses a two-key authentication model:
26
+ * - **Client credentials (Key A)**: Used to verify incoming requests from your clients
27
+ * - **Upstream credentials (Key B)**: Used to sign requests to the upstream S3 service
28
+ */
29
+ interface S3BrokerOptions {
30
+ /**
31
+ * The upstream S3-compatible endpoint URL.
32
+ *
33
+ * Supports AWS S3, Cloudflare R2, MinIO, and other S3-compatible services.
34
+ *
35
+ * @example 'https://s3.us-east-1.amazonaws.com'
36
+ * @example 'https://account-id.r2.cloudflarestorage.com'
37
+ */
38
+ s3Endpoint: string;
39
+ /**
40
+ * Access Key ID for client authentication (Key A)
41
+ * Used to verify incoming requests
42
+ */
43
+ clientAccessKeyId: string;
44
+ /**
45
+ * Secret Access Key for client authentication (Key A)
46
+ * Used to verify incoming requests
47
+ */
48
+ clientSecretAccessKey: string;
49
+ /**
50
+ * Access Key ID for upstream authentication (Key B)
51
+ * Used to sign requests to the upstream S3 service
52
+ */
53
+ upstreamAccessKeyId: string;
54
+ /**
55
+ * Secret Access Key for upstream authentication (Key B)
56
+ * Used to sign requests to the upstream S3 service
57
+ */
58
+ upstreamSecretAccessKey: string;
59
+ /**
60
+ * Optional custom guardrail configuration
61
+ * If not provided, default configuration will be used
62
+ */
63
+ guardrailConfig?: GuardrailConfig;
64
+ }
65
+ //# sourceMappingURL=types.d.ts.map
66
+ //#endregion
67
+ //#region src/index.d.ts
68
+ declare const defaultGuardrailConfig: GuardrailConfig;
69
+ /**
70
+ * Handle an incoming S3 request with signature verification, guardrails, and proxying.
71
+ *
72
+ * @param request - The incoming HTTP request (must be a valid S3 API request)
73
+ * @param options - S3Broker configuration options including credentials and guardrails
74
+ * @returns Response from the upstream S3 service, or an error response if validation fails
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * import { handle } from 's3broker';
79
+ *
80
+ * const response = await handle(request, ctx, {
81
+ * s3Endpoint: 'https://my-bucket.s3.amazonaws.com',
82
+ * clientAccessKeyId: 'CLIENT_KEY',
83
+ * clientSecretAccessKey: 'CLIENT_SECRET',
84
+ * upstreamAccessKeyId: 'UPSTREAM_KEY',
85
+ * upstreamSecretAccessKey: 'UPSTREAM_SECRET',
86
+ * });
87
+ * ```
88
+ */
89
+ declare function handle(request: Request<unknown, IncomingRequestCfProperties>, options: S3BrokerOptions): Promise<Response>;
90
+ //# sourceMappingURL=index.d.ts.map
91
+ //#endregion
92
+ export { type GuardrailConfig, GuardrailConfig as GuardrailConfigZod, type GuardrailViolation, type S3BrokerOptions, defaultGuardrailConfig, handle };
93
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,93 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/guardrails/type.d.ts
4
+
5
+ type GuardrailViolation = {
6
+ violation: string;
7
+ };
8
+ declare const GuardrailConfig: z.ZodObject<{
9
+ noDeleteOld: z.ZodArray<z.ZodObject<{
10
+ pattern: z.ZodString;
11
+ config: z.ZodNullable<z.ZodObject<{
12
+ noDeleteBeforeSeconds: z.ZodNumber;
13
+ }, z.core.$strip>>;
14
+ }, z.core.$strip>>;
15
+ }, z.core.$strip>;
16
+ type GuardrailConfig = z.infer<typeof GuardrailConfig>;
17
+ /**
18
+ * Error calling upstream when evaluating guardrails
19
+ */
20
+ //#endregion
21
+ //#region src/types.d.ts
22
+ /**
23
+ * Configuration options for the S3Broker handler.
24
+ *
25
+ * S3Broker uses a two-key authentication model:
26
+ * - **Client credentials (Key A)**: Used to verify incoming requests from your clients
27
+ * - **Upstream credentials (Key B)**: Used to sign requests to the upstream S3 service
28
+ */
29
+ interface S3BrokerOptions {
30
+ /**
31
+ * The upstream S3-compatible endpoint URL.
32
+ *
33
+ * Supports AWS S3, Cloudflare R2, MinIO, and other S3-compatible services.
34
+ *
35
+ * @example 'https://s3.us-east-1.amazonaws.com'
36
+ * @example 'https://account-id.r2.cloudflarestorage.com'
37
+ */
38
+ s3Endpoint: string;
39
+ /**
40
+ * Access Key ID for client authentication (Key A)
41
+ * Used to verify incoming requests
42
+ */
43
+ clientAccessKeyId: string;
44
+ /**
45
+ * Secret Access Key for client authentication (Key A)
46
+ * Used to verify incoming requests
47
+ */
48
+ clientSecretAccessKey: string;
49
+ /**
50
+ * Access Key ID for upstream authentication (Key B)
51
+ * Used to sign requests to the upstream S3 service
52
+ */
53
+ upstreamAccessKeyId: string;
54
+ /**
55
+ * Secret Access Key for upstream authentication (Key B)
56
+ * Used to sign requests to the upstream S3 service
57
+ */
58
+ upstreamSecretAccessKey: string;
59
+ /**
60
+ * Optional custom guardrail configuration
61
+ * If not provided, default configuration will be used
62
+ */
63
+ guardrailConfig?: GuardrailConfig;
64
+ }
65
+ //# sourceMappingURL=types.d.ts.map
66
+ //#endregion
67
+ //#region src/index.d.ts
68
+ declare const defaultGuardrailConfig: GuardrailConfig;
69
+ /**
70
+ * Handle an incoming S3 request with signature verification, guardrails, and proxying.
71
+ *
72
+ * @param request - The incoming HTTP request (must be a valid S3 API request)
73
+ * @param options - S3Broker configuration options including credentials and guardrails
74
+ * @returns Response from the upstream S3 service, or an error response if validation fails
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * import { handle } from 's3broker';
79
+ *
80
+ * const response = await handle(request, ctx, {
81
+ * s3Endpoint: 'https://my-bucket.s3.amazonaws.com',
82
+ * clientAccessKeyId: 'CLIENT_KEY',
83
+ * clientSecretAccessKey: 'CLIENT_SECRET',
84
+ * upstreamAccessKeyId: 'UPSTREAM_KEY',
85
+ * upstreamSecretAccessKey: 'UPSTREAM_SECRET',
86
+ * });
87
+ * ```
88
+ */
89
+ declare function handle(request: Request<unknown, IncomingRequestCfProperties>, options: S3BrokerOptions): Promise<Response>;
90
+ //# sourceMappingURL=index.d.ts.map
91
+ //#endregion
92
+ export { type GuardrailConfig, GuardrailConfig as GuardrailConfigZod, type GuardrailViolation, type S3BrokerOptions, defaultGuardrailConfig, handle };
93
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
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 ADDED
@@ -0,0 +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.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 ADDED
@@ -0,0 +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.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
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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 * 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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"}
@@ -0,0 +1,4 @@
1
+ function e(e){let t=new URL(e.url);return t.searchParams.has(`X-Amz-Signature`)}function t(e){let t=new URL(e.url),n=t.searchParams.get(`X-Amz-Algorithm`),r=t.searchParams.get(`X-Amz-Credential`),i=t.searchParams.get(`X-Amz-SignedHeaders`),a=t.searchParams.get(`X-Amz-Signature`),o=t.searchParams.get(`X-Amz-Expires`);if(!n||n!==`AWS4-HMAC-SHA256`)throw Error(`Invalid or missing X-Amz-Algorithm`);if(!r||!i||!a)throw Error(`Missing required presigned URL parameters`);let s=r.split(`/`);if(s.length!==5||s[4]!==`aws4_request`)throw Error(`Invalid credential format in presigned URL`);return{algorithm:`AWS4-HMAC-SHA256`,credential:{accessKeyId:s[0],date:s[1],region:s[2],service:s[3]},signedHeaders:i.split(`;`),signature:a,expires:o?parseInt(o,10):void 0,isPresigned:!0}}function n(e){if(!e.startsWith(`AWS4-HMAC-SHA256 `))throw Error(`Invalid authorization header: must start with AWS4-HMAC-SHA256`);let t=e.substring(17).split(`,`).map(e=>e.trim()),n={};for(let e of t){let[t,r]=e.split(`=`,2);n[t]=r}if(!n.Credential||!n.SignedHeaders||!n.Signature)throw Error(`Missing required authorization parameters`);let r=n.Credential.split(`/`);if(r.length!==5||r[4]!==`aws4_request`)throw Error(`Invalid credential format`);return{algorithm:`AWS4-HMAC-SHA256`,credential:{accessKeyId:r[0],date:r[1],region:r[2],service:r[3]},signedHeaders:n.SignedHeaders.split(`;`),signature:n.Signature,isPresigned:!1}}async function r(e,t,n,r=!1){let i=new URL(e.url),a=e.method,o=i.pathname||`/`,s=Array.from(i.searchParams.entries()).filter(([e])=>!r||e!==`X-Amz-Signature`).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${encodeURIComponent(e)}=${encodeURIComponent(t)}`).join(`&`),c={};for(let n of t){let t=e.headers.get(n);t!==null&&(c[n.toLowerCase()]=t.trim())}let l=t.map(e=>`${e.toLowerCase()}:${c[e.toLowerCase()]||``}\n`).join(``),u=t.map(e=>e.toLowerCase()).join(`;`);return[a,o,s,l,u,n].join(`
2
+ `)}async function i(e,t,n,r){let i=new TextEncoder,a=await crypto.subtle.digest(`SHA-256`,i.encode(r)),o=Array.from(new Uint8Array(a)).map(e=>e.toString(16).padStart(2,`0`)).join(``);return[e,t,n,o].join(`
3
+ `)}async function a(e,t,n,r){let i=new TextEncoder,a=await o(i.encode(`AWS4`+e),i.encode(t)),s=await o(a,i.encode(n)),c=await o(s,i.encode(r)),l=await o(c,i.encode(`aws4_request`));return l}async function o(e,t){let n=await crypto.subtle.importKey(`raw`,e,{name:`HMAC`,hash:`SHA-256`},!1,[`sign`]);return crypto.subtle.sign(`HMAC`,n,t)}async function s(e,t){let n=new TextEncoder,r=await o(e,n.encode(t));return Array.from(new Uint8Array(r)).map(e=>e.toString(16).padStart(2,`0`)).join(``)}async function c(e,t){if(e.length!==t.length)return!1;let n=new TextEncoder,r=n.encode(e),i=n.encode(t);try{return await crypto.subtle.timingSafeEqual(r,i)}catch{return!1}}function l(e){let t=parseInt(e.substring(0,4)),n=parseInt(e.substring(4,6))-1,r=parseInt(e.substring(6,8)),i=parseInt(e.substring(9,11)),a=parseInt(e.substring(11,13)),o=parseInt(e.substring(13,15));return Date.UTC(t,n,r,i,a,o)}async function u(e,o,u,d){try{let f=new URL(e.url),p=e.headers.get(`Authorization`),m=f.searchParams.has(`X-Amz-Signature`),h;if(m)h=t(e);else if(p)h=n(p);else return{valid:!1,error:`Missing authentication (no Authorization header or presigned URL parameters)`};if(h.credential.accessKeyId!==u)return{valid:!1,error:`Access key ID mismatch`};let g;if(g=m?f.searchParams.get(`X-Amz-Date`):e.headers.get(`x-amz-date`),!g)return{valid:!1,error:`Missing request date (x-amz-date)`};if(m){if(m&&h.expires!==void 0){let e=l(g),t=e+h.expires*1e3;if(d>t)return{valid:!1,error:`Presigned URL has expired`}}}else{let e=l(g),t=5*60*1e3;if(Math.abs(d-e)>t)return{valid:!1,error:`Request date too old or in future (max 5 min clock skew)`}}let _;if(_=m?`UNSIGNED-PAYLOAD`:e.headers.get(`x-amz-content-sha256`)||`UNSIGNED-PAYLOAD`,_===`STREAMING-AWS4-HMAC-SHA256-PAYLOAD`)return{valid:!1,error:`Streaming payload signatures are not supported`};let v=await r(e,h.signedHeaders,_,m),y=`${h.credential.date}/${h.credential.region}/${h.credential.service}/aws4_request`,b=await i(h.algorithm,g,y,v),x=await a(o,h.credential.date,h.credential.region,h.credential.service),S=await s(x,b),C=await c(S,h.signature);return C?{valid:!0,isPresigned:m}:{valid:!1,error:`Signature mismatch`,isPresigned:m}}catch(e){return{valid:!1,error:e instanceof Error?e.message:`Unknown error`}}}export{r as buildCanonicalRequest,s as calculateSignature,i as createStringToSign,a as deriveSigningKey,e as isPresignedUrl,n as parseAuthorizationHeader,t as parsePresignedUrl,u as verifySignature};
4
+ //# sourceMappingURL=sigv4-Bh65MQC2.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sigv4-Bh65MQC2.mjs","names":["request: Request","authHeader: string","params: Record<string, string>","signedHeaders: string[]","hashedPayload: string","isPresigned: boolean","headers: Record<string, string>","algorithm: string","requestDate: string","credentialScope: string","canonicalRequest: string","secretKey: string","date: string","region: string","service: string","key: ArrayBuffer | Uint8Array","data: Uint8Array","signingKey: ArrayBuffer","stringToSign: string","a: string","b: string","dateStr: string","clientSecretKey: string","expectedAccessKeyId: string","currentTimestampMs: number","params: SigV4Params","requestDate: string | null","payloadHash: string"],"sources":["../src/sigv4.ts"],"sourcesContent":["/**\n * AWS Signature Version 4 verification utilities\n *\n * This module provides functions to verify incoming S3 requests signed with SigV4.\n * It parses the Authorization header, reconstructs the canonical request, and\n * verifies the signature matches what we expect.\n */\n\ninterface SigV4Params {\n\talgorithm: string;\n\tcredential: {\n\t\taccessKeyId: string;\n\t\tdate: string;\n\t\tregion: string;\n\t\tservice: string;\n\t};\n\tsignedHeaders: string[];\n\tsignature: string;\n\t// Presigned URL specific\n\texpires?: number;\n\tisPresigned?: boolean;\n}\n\n/**\n * Check if request uses presigned URL authentication\n */\nexport function isPresignedUrl(request: Request): boolean {\n\tconst url = new URL(request.url);\n\treturn url.searchParams.has('X-Amz-Signature');\n}\n\n/**\n * Parse presigned URL query parameters\n * Query params: X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders, X-Amz-Signature\n */\nexport function parsePresignedUrl(request: Request): SigV4Params {\n\tconst url = new URL(request.url);\n\n\tconst algorithm = url.searchParams.get('X-Amz-Algorithm');\n\tconst credential = url.searchParams.get('X-Amz-Credential');\n\tconst signedHeaders = url.searchParams.get('X-Amz-SignedHeaders');\n\tconst signature = url.searchParams.get('X-Amz-Signature');\n\tconst expires = url.searchParams.get('X-Amz-Expires');\n\n\tif (!algorithm || algorithm !== 'AWS4-HMAC-SHA256') {\n\t\tthrow new Error('Invalid or missing X-Amz-Algorithm');\n\t}\n\tif (!credential || !signedHeaders || !signature) {\n\t\tthrow new Error('Missing required presigned URL parameters');\n\t}\n\n\t// Parse credential: accessKeyId/date/region/service/aws4_request\n\tconst credentialParts = credential.split('/');\n\tif (credentialParts.length !== 5 || credentialParts[4] !== 'aws4_request') {\n\t\tthrow new Error('Invalid credential format in presigned URL');\n\t}\n\n\treturn {\n\t\talgorithm: 'AWS4-HMAC-SHA256',\n\t\tcredential: {\n\t\t\taccessKeyId: credentialParts[0],\n\t\t\tdate: credentialParts[1],\n\t\t\tregion: credentialParts[2],\n\t\t\tservice: credentialParts[3],\n\t\t},\n\t\tsignedHeaders: signedHeaders.split(';'),\n\t\tsignature: signature,\n\t\texpires: expires ? parseInt(expires, 10) : undefined,\n\t\tisPresigned: true,\n\t};\n}\n\n/**\n * Parse the AWS SigV4 Authorization header\n * Format: AWS4-HMAC-SHA256 Credential=AKID/20231224/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=...\n */\nexport function parseAuthorizationHeader(authHeader: string): SigV4Params {\n\tif (!authHeader.startsWith('AWS4-HMAC-SHA256 ')) {\n\t\tthrow new Error('Invalid authorization header: must start with AWS4-HMAC-SHA256');\n\t}\n\n\t// Split by comma and trim whitespace to handle both \"key=value, key=value\" and \"key=value,key=value\"\n\tconst parts = authHeader\n\t\t.substring('AWS4-HMAC-SHA256 '.length)\n\t\t.split(',')\n\t\t.map((p) => p.trim());\n\tconst params: Record<string, string> = {};\n\n\tfor (const part of parts) {\n\t\tconst [key, value] = part.split('=', 2);\n\t\tparams[key] = value;\n\t}\n\n\tif (!params.Credential || !params.SignedHeaders || !params.Signature) {\n\t\tthrow new Error('Missing required authorization parameters');\n\t}\n\n\t// Parse credential: accessKeyId/date/region/service/aws4_request\n\tconst credentialParts = params.Credential.split('/');\n\tif (credentialParts.length !== 5 || credentialParts[4] !== 'aws4_request') {\n\t\tthrow new Error('Invalid credential format');\n\t}\n\n\treturn {\n\t\talgorithm: 'AWS4-HMAC-SHA256',\n\t\tcredential: {\n\t\t\taccessKeyId: credentialParts[0],\n\t\t\tdate: credentialParts[1],\n\t\t\tregion: credentialParts[2],\n\t\t\tservice: credentialParts[3],\n\t\t},\n\t\tsignedHeaders: params.SignedHeaders.split(';'),\n\t\tsignature: params.Signature,\n\t\tisPresigned: false,\n\t};\n}\n\n/**\n * Build the canonical request from the incoming request\n * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html\n *\n * @param isPresigned - If true, exclude X-Amz-Signature from query string\n */\nexport async function buildCanonicalRequest(\n\trequest: Request,\n\tsignedHeaders: string[],\n\thashedPayload: string,\n\tisPresigned: boolean = false,\n): Promise<string> {\n\tconst url = new URL(request.url);\n\tconst httpMethod = request.method;\n\n\t// Canonical URI (path)\n\tconst canonicalUri = url.pathname || '/';\n\n\t// Canonical query string (sorted, exclude X-Amz-Signature for presigned URLs)\n\tconst canonicalQueryString = Array.from(url.searchParams.entries())\n\t\t.filter(([key]) => !isPresigned || key !== 'X-Amz-Signature')\n\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n\t\t.join('&');\n\n\t// Canonical headers (lowercase, sorted, trimmed)\n\tconst headers: Record<string, string> = {};\n\tfor (const headerName of signedHeaders) {\n\t\tconst value = request.headers.get(headerName);\n\t\tif (value !== null) {\n\t\t\theaders[headerName.toLowerCase()] = value.trim();\n\t\t}\n\t}\n\n\tconst canonicalHeaders = signedHeaders.map((name) => `${name.toLowerCase()}:${headers[name.toLowerCase()] || ''}\\n`).join('');\n\n\tconst canonicalSignedHeaders = signedHeaders.map((h) => h.toLowerCase()).join(';');\n\n\t// Combine into canonical request\n\treturn [httpMethod, canonicalUri, canonicalQueryString, canonicalHeaders, canonicalSignedHeaders, hashedPayload].join('\\n');\n}\n\n/**\n * Create the string to sign\n * https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html\n */\nexport async function createStringToSign(\n\talgorithm: string,\n\trequestDate: string,\n\tcredentialScope: string,\n\tcanonicalRequest: string,\n): Promise<string> {\n\tconst encoder = new TextEncoder();\n\tconst canonicalRequestHash = await crypto.subtle.digest('SHA-256', encoder.encode(canonicalRequest));\n\tconst canonicalRequestHashHex = Array.from(new Uint8Array(canonicalRequestHash))\n\t\t.map((b) => b.toString(16).padStart(2, '0'))\n\t\t.join('');\n\n\treturn [algorithm, requestDate, credentialScope, canonicalRequestHashHex].join('\\n');\n}\n\n/**\n * Derive the signing key\n * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html\n */\nexport async function deriveSigningKey(secretKey: string, date: string, region: string, service: string): Promise<ArrayBuffer> {\n\tconst encoder = new TextEncoder();\n\n\tconst kDate = await hmacSha256(encoder.encode('AWS4' + secretKey), encoder.encode(date));\n\tconst kRegion = await hmacSha256(kDate, encoder.encode(region));\n\tconst kService = await hmacSha256(kRegion, encoder.encode(service));\n\tconst kSigning = await hmacSha256(kService, encoder.encode('aws4_request'));\n\n\treturn kSigning;\n}\n\n/**\n * HMAC-SHA256 helper\n */\nasync function hmacSha256(key: ArrayBuffer | Uint8Array, data: Uint8Array): Promise<ArrayBuffer> {\n\tconst cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);\n\treturn crypto.subtle.sign('HMAC', cryptoKey, data);\n}\n\n/**\n * Calculate the signature\n */\nexport async function calculateSignature(signingKey: ArrayBuffer, stringToSign: string): Promise<string> {\n\tconst encoder = new TextEncoder();\n\tconst signature = await hmacSha256(signingKey, encoder.encode(stringToSign));\n\treturn Array.from(new Uint8Array(signature))\n\t\t.map((b) => b.toString(16).padStart(2, '0'))\n\t\t.join('');\n}\n\n/**\n * Constant-time string comparison using crypto.subtle.timingSafeEqual\n * Prevents timing attacks on signature comparison\n */\nasync function constantTimeCompare(a: string, b: string): Promise<boolean> {\n\tif (a.length !== b.length) return false;\n\n\tconst encoder = new TextEncoder();\n\tconst aBytes = encoder.encode(a);\n\tconst bBytes = encoder.encode(b);\n\n\ttry {\n\t\treturn await crypto.subtle.timingSafeEqual(aBytes, bBytes);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Parse AWS date format (YYYYMMDDTHHMMSSZ) to timestamp (ms)\n */\nfunction parseAmzDate(dateStr: string): number {\n\tconst year = parseInt(dateStr.substring(0, 4));\n\tconst month = parseInt(dateStr.substring(4, 6)) - 1;\n\tconst day = parseInt(dateStr.substring(6, 8));\n\tconst hour = parseInt(dateStr.substring(9, 11));\n\tconst minute = parseInt(dateStr.substring(11, 13));\n\tconst second = parseInt(dateStr.substring(13, 15));\n\treturn Date.UTC(year, month, day, hour, minute, second);\n}\n\n/**\n * Verify the signature of an incoming request\n * Supports both Authorization header and presigned URL authentication\n */\nexport async function verifySignature(\n\trequest: Request,\n\tclientSecretKey: string,\n\texpectedAccessKeyId: string,\n\tcurrentTimestampMs: number,\n): Promise<{ valid: boolean; error?: string; isPresigned?: boolean }> {\n\ttry {\n\t\tconst url = new URL(request.url);\n\t\tconst authHeader = request.headers.get('Authorization');\n\t\tconst isPresigned = url.searchParams.has('X-Amz-Signature');\n\n\t\t// Parse auth params from either header or query parameters\n\t\tlet params: SigV4Params;\n\t\tif (isPresigned) {\n\t\t\tparams = parsePresignedUrl(request);\n\t\t} else if (authHeader) {\n\t\t\tparams = parseAuthorizationHeader(authHeader);\n\t\t} else {\n\t\t\treturn { valid: false, error: 'Missing authentication (no Authorization header or presigned URL parameters)' };\n\t\t}\n\n\t\t// Verify access key ID matches\n\t\tif (params.credential.accessKeyId !== expectedAccessKeyId) {\n\t\t\treturn { valid: false, error: 'Access key ID mismatch' };\n\t\t}\n\n\t\t// Get request date early for both staleness and expiration checks\n\t\tlet requestDate: string | null;\n\t\tif (isPresigned) {\n\t\t\trequestDate = url.searchParams.get('X-Amz-Date');\n\t\t} else {\n\t\t\trequestDate = request.headers.get('x-amz-date');\n\t\t}\n\n\t\tif (!requestDate) {\n\t\t\treturn { valid: false, error: 'Missing request date (x-amz-date)' };\n\t\t}\n\n\t\t// Check request date staleness to prevent replay attacks\n\t\t// For presigned URLs, expiration is checked separately below\n\t\tif (!isPresigned) {\n\t\t\tconst requestDateMs = parseAmzDate(requestDate);\n\t\t\tconst MAX_CLOCK_SKEW_MS = 5 * 60 * 1000; // 5 minutes\n\n\t\t\tif (Math.abs(currentTimestampMs - requestDateMs) > MAX_CLOCK_SKEW_MS) {\n\t\t\t\treturn { valid: false, error: 'Request date too old or in future (max 5 min clock skew)' };\n\t\t\t}\n\t\t}\n\t\t// For presigned URLs, check expiration\n\t\telse if (isPresigned && params.expires !== undefined) {\n\t\t\tconst requestDateMs = parseAmzDate(requestDate);\n\t\t\tconst expiresAt = requestDateMs + params.expires * 1000;\n\n\t\t\tif (currentTimestampMs > expiresAt) {\n\t\t\t\treturn { valid: false, error: 'Presigned URL has expired' };\n\t\t\t}\n\t\t}\n\n\t\t// Get the payload hash\n\t\tlet payloadHash: string;\n\t\tif (isPresigned) {\n\t\t\t// Presigned URLs always use UNSIGNED-PAYLOAD\n\t\t\tpayloadHash = 'UNSIGNED-PAYLOAD';\n\t\t} else {\n\t\t\tpayloadHash = request.headers.get('x-amz-content-sha256') || 'UNSIGNED-PAYLOAD';\n\t\t}\n\n\t\t// Check for streaming payload (not supported)\n\t\tif (payloadHash === 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD') {\n\t\t\treturn { valid: false, error: 'Streaming payload signatures are not supported' };\n\t\t}\n\n\t\t// Build canonical request\n\t\tconst canonicalRequest = await buildCanonicalRequest(request, params.signedHeaders, payloadHash, isPresigned);\n\n\t\t// Create credential scope\n\t\tconst credentialScope = `${params.credential.date}/${params.credential.region}/${params.credential.service}/aws4_request`;\n\n\t\t// Create string to sign\n\t\tconst stringToSign = await createStringToSign(params.algorithm, requestDate, credentialScope, canonicalRequest);\n\n\t\t// Derive signing key\n\t\tconst signingKey = await deriveSigningKey(clientSecretKey, params.credential.date, params.credential.region, params.credential.service);\n\n\t\t// Calculate expected signature\n\t\tconst expectedSignature = await calculateSignature(signingKey, stringToSign);\n\n\t\t// Compare signatures using constant-time comparison to prevent timing attacks\n\t\tconst signatureValid = await constantTimeCompare(expectedSignature, params.signature);\n\n\t\tif (!signatureValid) {\n\t\t\treturn { valid: false, error: 'Signature mismatch', isPresigned };\n\t\t}\n\n\t\t// SECURITY NOTE: We do not validate the 'host' header against an expected value.\n\t\t// If you need to restrict which domains can use these credentials, consider adding:\n\t\t// - An environment variable for expected host(s)\n\t\t// - Validation that request.headers.get('host') matches the expected value\n\t\t// This would prevent credentials from being used on different worker domains.\n\n\t\treturn { valid: true, isPresigned };\n\t} catch (error) {\n\t\treturn { valid: false, error: error instanceof Error ? error.message : 'Unknown error' };\n\t}\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ function e(e){let t=new URL(e.url);return t.searchParams.has(`X-Amz-Signature`)}function t(e){let t=new URL(e.url),n=t.searchParams.get(`X-Amz-Algorithm`),r=t.searchParams.get(`X-Amz-Credential`),i=t.searchParams.get(`X-Amz-SignedHeaders`),a=t.searchParams.get(`X-Amz-Signature`),o=t.searchParams.get(`X-Amz-Expires`);if(!n||n!==`AWS4-HMAC-SHA256`)throw Error(`Invalid or missing X-Amz-Algorithm`);if(!r||!i||!a)throw Error(`Missing required presigned URL parameters`);let s=r.split(`/`);if(s.length!==5||s[4]!==`aws4_request`)throw Error(`Invalid credential format in presigned URL`);return{algorithm:`AWS4-HMAC-SHA256`,credential:{accessKeyId:s[0],date:s[1],region:s[2],service:s[3]},signedHeaders:i.split(`;`),signature:a,expires:o?parseInt(o,10):void 0,isPresigned:!0}}function n(e){if(!e.startsWith(`AWS4-HMAC-SHA256 `))throw Error(`Invalid authorization header: must start with AWS4-HMAC-SHA256`);let t=e.substring(17).split(`,`).map(e=>e.trim()),n={};for(let e of t){let[t,r]=e.split(`=`,2);n[t]=r}if(!n.Credential||!n.SignedHeaders||!n.Signature)throw Error(`Missing required authorization parameters`);let r=n.Credential.split(`/`);if(r.length!==5||r[4]!==`aws4_request`)throw Error(`Invalid credential format`);return{algorithm:`AWS4-HMAC-SHA256`,credential:{accessKeyId:r[0],date:r[1],region:r[2],service:r[3]},signedHeaders:n.SignedHeaders.split(`;`),signature:n.Signature,isPresigned:!1}}async function r(e,t,n,r=!1){let i=new URL(e.url),a=e.method,o=i.pathname||`/`,s=Array.from(i.searchParams.entries()).filter(([e])=>!r||e!==`X-Amz-Signature`).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${encodeURIComponent(e)}=${encodeURIComponent(t)}`).join(`&`),c={};for(let n of t){let t=e.headers.get(n);t!==null&&(c[n.toLowerCase()]=t.trim())}let l=t.map(e=>`${e.toLowerCase()}:${c[e.toLowerCase()]||``}\n`).join(``),u=t.map(e=>e.toLowerCase()).join(`;`);return[a,o,s,l,u,n].join(`
2
+ `)}async function i(e,t,n,r){let i=new TextEncoder,a=await crypto.subtle.digest(`SHA-256`,i.encode(r)),o=Array.from(new Uint8Array(a)).map(e=>e.toString(16).padStart(2,`0`)).join(``);return[e,t,n,o].join(`
3
+ `)}async function a(e,t,n,r){let i=new TextEncoder,a=await o(i.encode(`AWS4`+e),i.encode(t)),s=await o(a,i.encode(n)),c=await o(s,i.encode(r)),l=await o(c,i.encode(`aws4_request`));return l}async function o(e,t){let n=await crypto.subtle.importKey(`raw`,e,{name:`HMAC`,hash:`SHA-256`},!1,[`sign`]);return crypto.subtle.sign(`HMAC`,n,t)}async function s(e,t){let n=new TextEncoder,r=await o(e,n.encode(t));return Array.from(new Uint8Array(r)).map(e=>e.toString(16).padStart(2,`0`)).join(``)}async function c(e,t){if(e.length!==t.length)return!1;let n=new TextEncoder,r=n.encode(e),i=n.encode(t);try{return await crypto.subtle.timingSafeEqual(r,i)}catch{return!1}}function l(e){let t=parseInt(e.substring(0,4)),n=parseInt(e.substring(4,6))-1,r=parseInt(e.substring(6,8)),i=parseInt(e.substring(9,11)),a=parseInt(e.substring(11,13)),o=parseInt(e.substring(13,15));return Date.UTC(t,n,r,i,a,o)}async function u(e,o,u,d){try{let f=new URL(e.url),p=e.headers.get(`Authorization`),m=f.searchParams.has(`X-Amz-Signature`),h;if(m)h=t(e);else if(p)h=n(p);else return{valid:!1,error:`Missing authentication (no Authorization header or presigned URL parameters)`};if(h.credential.accessKeyId!==u)return{valid:!1,error:`Access key ID mismatch`};let g;if(g=m?f.searchParams.get(`X-Amz-Date`):e.headers.get(`x-amz-date`),!g)return{valid:!1,error:`Missing request date (x-amz-date)`};if(m){if(m&&h.expires!==void 0){let e=l(g),t=e+h.expires*1e3;if(d>t)return{valid:!1,error:`Presigned URL has expired`}}}else{let e=l(g),t=5*60*1e3;if(Math.abs(d-e)>t)return{valid:!1,error:`Request date too old or in future (max 5 min clock skew)`}}let _;if(_=m?`UNSIGNED-PAYLOAD`:e.headers.get(`x-amz-content-sha256`)||`UNSIGNED-PAYLOAD`,_===`STREAMING-AWS4-HMAC-SHA256-PAYLOAD`)return{valid:!1,error:`Streaming payload signatures are not supported`};let v=await r(e,h.signedHeaders,_,m),y=`${h.credential.date}/${h.credential.region}/${h.credential.service}/aws4_request`,b=await i(h.algorithm,g,y,v),x=await a(o,h.credential.date,h.credential.region,h.credential.service),S=await s(x,b),C=await c(S,h.signature);return C?{valid:!0,isPresigned:m}:{valid:!1,error:`Signature mismatch`,isPresigned:m}}catch(e){return{valid:!1,error:e instanceof Error?e.message:`Unknown error`}}}Object.defineProperty(exports,`buildCanonicalRequest`,{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,`calculateSignature`,{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,`createStringToSign`,{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,`deriveSigningKey`,{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,`isPresignedUrl`,{enumerable:!0,get:function(){return e}}),Object.defineProperty(exports,`parseAuthorizationHeader`,{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,`parsePresignedUrl`,{enumerable:!0,get:function(){return t}}),Object.defineProperty(exports,`verifySignature`,{enumerable:!0,get:function(){return u}});
@@ -0,0 +1,68 @@
1
+ //#region src/sigv4.d.ts
2
+ /**
3
+ * AWS Signature Version 4 verification utilities
4
+ *
5
+ * This module provides functions to verify incoming S3 requests signed with SigV4.
6
+ * It parses the Authorization header, reconstructs the canonical request, and
7
+ * verifies the signature matches what we expect.
8
+ */
9
+ interface SigV4Params {
10
+ algorithm: string;
11
+ credential: {
12
+ accessKeyId: string;
13
+ date: string;
14
+ region: string;
15
+ service: string;
16
+ };
17
+ signedHeaders: string[];
18
+ signature: string;
19
+ expires?: number;
20
+ isPresigned?: boolean;
21
+ }
22
+ /**
23
+ * Check if request uses presigned URL authentication
24
+ */
25
+ declare function isPresignedUrl(request: Request): boolean;
26
+ /**
27
+ * Parse presigned URL query parameters
28
+ * Query params: X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders, X-Amz-Signature
29
+ */
30
+ declare function parsePresignedUrl(request: Request): SigV4Params;
31
+ /**
32
+ * Parse the AWS SigV4 Authorization header
33
+ * Format: AWS4-HMAC-SHA256 Credential=AKID/20231224/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=...
34
+ */
35
+ declare function parseAuthorizationHeader(authHeader: string): SigV4Params;
36
+ /**
37
+ * Build the canonical request from the incoming request
38
+ * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
39
+ *
40
+ * @param isPresigned - If true, exclude X-Amz-Signature from query string
41
+ */
42
+ declare function buildCanonicalRequest(request: Request, signedHeaders: string[], hashedPayload: string, isPresigned?: boolean): Promise<string>;
43
+ /**
44
+ * Create the string to sign
45
+ * https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
46
+ */
47
+ declare function createStringToSign(algorithm: string, requestDate: string, credentialScope: string, canonicalRequest: string): Promise<string>;
48
+ /**
49
+ * Derive the signing key
50
+ * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
51
+ */
52
+ declare function deriveSigningKey(secretKey: string, date: string, region: string, service: string): Promise<ArrayBuffer>;
53
+ /**
54
+ * Calculate the signature
55
+ */
56
+ declare function calculateSignature(signingKey: ArrayBuffer, stringToSign: string): Promise<string>;
57
+ /**
58
+ * Verify the signature of an incoming request
59
+ * Supports both Authorization header and presigned URL authentication
60
+ */
61
+ declare function verifySignature(request: Request, clientSecretKey: string, expectedAccessKeyId: string, currentTimestampMs: number): Promise<{
62
+ valid: boolean;
63
+ error?: string;
64
+ isPresigned?: boolean;
65
+ }>;
66
+ //#endregion
67
+ export { buildCanonicalRequest, calculateSignature, createStringToSign, deriveSigningKey, isPresignedUrl, parseAuthorizationHeader, parsePresignedUrl, verifySignature };
68
+ //# sourceMappingURL=sigv4.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sigv4.d.mts","names":[],"sources":["../src/sigv4.ts"],"sourcesContent":[],"mappings":";;;AA0BA;AASA;;;;AAAgE,UA3BtD,WAAA,CA2BsD;EAyChD,SAAA,EAAA,MAAA;EA+CM,UAAA,EAAA;IAAqB,WAAA,EAAA,MAAA;IACjC,IAAA,EAAA,MAAA;IAIP,MAAA,EAAA,MAAA;IAAO,OAAA,EAAA,MAAA;EAmCY,CAAA;EAmBA,aAAA,EAAA,MAAgB,EAAA;EAAA,SAAA,EAAA,MAAA;EAAA,OAA4E,CAAA,EAAA,MAAA;EAAW,WAAnB,CAAA,EAAA,OAAA;AAAO;AAsBjH;;;AAAyF,iBAlLzE,cAAA,CAkLyE,OAAA,EAlLjD,OAkLiD,CAAA,EAAA,OAAA;AAAO;AA2ChG;;;AAKG,iBAzNa,iBAAA,CAyNb,OAAA,EAzNwC,OAyNxC,CAAA,EAzNkD,WAyNlD;AAAO;;;;iBAhLM,wBAAA,sBAA8C;;;;;;;iBA+CxC,qBAAA,UACZ,iFAIP;;;;;iBAmCmB,kBAAA,6FAKnB;;;;;iBAcmB,gBAAA,oEAAoF,QAAQ;;;;iBAsB5F,kBAAA,aAA+B,oCAAoC;;;;;iBA2CnE,eAAA,UACZ,4FAIP"}
@@ -0,0 +1,68 @@
1
+ //#region src/sigv4.d.ts
2
+ /**
3
+ * AWS Signature Version 4 verification utilities
4
+ *
5
+ * This module provides functions to verify incoming S3 requests signed with SigV4.
6
+ * It parses the Authorization header, reconstructs the canonical request, and
7
+ * verifies the signature matches what we expect.
8
+ */
9
+ interface SigV4Params {
10
+ algorithm: string;
11
+ credential: {
12
+ accessKeyId: string;
13
+ date: string;
14
+ region: string;
15
+ service: string;
16
+ };
17
+ signedHeaders: string[];
18
+ signature: string;
19
+ expires?: number;
20
+ isPresigned?: boolean;
21
+ }
22
+ /**
23
+ * Check if request uses presigned URL authentication
24
+ */
25
+ declare function isPresignedUrl(request: Request): boolean;
26
+ /**
27
+ * Parse presigned URL query parameters
28
+ * Query params: X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders, X-Amz-Signature
29
+ */
30
+ declare function parsePresignedUrl(request: Request): SigV4Params;
31
+ /**
32
+ * Parse the AWS SigV4 Authorization header
33
+ * Format: AWS4-HMAC-SHA256 Credential=AKID/20231224/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=...
34
+ */
35
+ declare function parseAuthorizationHeader(authHeader: string): SigV4Params;
36
+ /**
37
+ * Build the canonical request from the incoming request
38
+ * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
39
+ *
40
+ * @param isPresigned - If true, exclude X-Amz-Signature from query string
41
+ */
42
+ declare function buildCanonicalRequest(request: Request, signedHeaders: string[], hashedPayload: string, isPresigned?: boolean): Promise<string>;
43
+ /**
44
+ * Create the string to sign
45
+ * https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
46
+ */
47
+ declare function createStringToSign(algorithm: string, requestDate: string, credentialScope: string, canonicalRequest: string): Promise<string>;
48
+ /**
49
+ * Derive the signing key
50
+ * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
51
+ */
52
+ declare function deriveSigningKey(secretKey: string, date: string, region: string, service: string): Promise<ArrayBuffer>;
53
+ /**
54
+ * Calculate the signature
55
+ */
56
+ declare function calculateSignature(signingKey: ArrayBuffer, stringToSign: string): Promise<string>;
57
+ /**
58
+ * Verify the signature of an incoming request
59
+ * Supports both Authorization header and presigned URL authentication
60
+ */
61
+ declare function verifySignature(request: Request, clientSecretKey: string, expectedAccessKeyId: string, currentTimestampMs: number): Promise<{
62
+ valid: boolean;
63
+ error?: string;
64
+ isPresigned?: boolean;
65
+ }>;
66
+ //#endregion
67
+ export { buildCanonicalRequest, calculateSignature, createStringToSign, deriveSigningKey, isPresignedUrl, parseAuthorizationHeader, parsePresignedUrl, verifySignature };
68
+ //# sourceMappingURL=sigv4.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sigv4.d.ts","names":[],"sources":["../src/sigv4.ts"],"sourcesContent":[],"mappings":";;;AA0BA;AASA;;;;AAAgE,UA3BtD,WAAA,CA2BsD;EAyChD,SAAA,EAAA,MAAA;EA+CM,UAAA,EAAA;IAAqB,WAAA,EAAA,MAAA;IACjC,IAAA,EAAA,MAAA;IAIP,MAAA,EAAA,MAAA;IAAO,OAAA,EAAA,MAAA;EAmCY,CAAA;EAmBA,aAAA,EAAA,MAAgB,EAAA;EAAA,SAAA,EAAA,MAAA;EAAA,OAA4E,CAAA,EAAA,MAAA;EAAW,WAAnB,CAAA,EAAA,OAAA;AAAO;AAsBjH;;;AAAyF,iBAlLzE,cAAA,CAkLyE,OAAA,EAlLjD,OAkLiD,CAAA,EAAA,OAAA;AAAO;AA2ChG;;;AAKG,iBAzNa,iBAAA,CAyNb,OAAA,EAzNwC,OAyNxC,CAAA,EAzNkD,WAyNlD;AAAO;;;;iBAhLM,wBAAA,sBAA8C;;;;;;;iBA+CxC,qBAAA,UACZ,iFAIP;;;;;iBAmCmB,kBAAA,6FAKnB;;;;;iBAcmB,gBAAA,oEAAoF,QAAQ;;;;iBAsB5F,kBAAA,aAA+B,oCAAoC;;;;;iBA2CnE,eAAA,UACZ,4FAIP"}
package/dist/sigv4.js ADDED
@@ -0,0 +1 @@
1
+ const e=require(`./sigv4-CT4jkHNc.js`);exports.buildCanonicalRequest=e.buildCanonicalRequest,exports.calculateSignature=e.calculateSignature,exports.createStringToSign=e.createStringToSign,exports.deriveSigningKey=e.deriveSigningKey,exports.isPresignedUrl=e.isPresignedUrl,exports.parseAuthorizationHeader=e.parseAuthorizationHeader,exports.parsePresignedUrl=e.parsePresignedUrl,exports.verifySignature=e.verifySignature;
package/dist/sigv4.mjs ADDED
@@ -0,0 +1 @@
1
+ import{buildCanonicalRequest as e,calculateSignature as t,createStringToSign as n,deriveSigningKey as r,isPresignedUrl as i,parseAuthorizationHeader as a,parsePresignedUrl as o,verifySignature as s}from"./sigv4-Bh65MQC2.mjs";export{e as buildCanonicalRequest,t as calculateSignature,n as createStringToSign,r as deriveSigningKey,i as isPresignedUrl,a as parseAuthorizationHeader,o as parsePresignedUrl,s as verifySignature};
package/package.json CHANGED
@@ -1,11 +1,30 @@
1
1
  {
2
2
  "name": "s3broker",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "S3 proxy library with SigV4 verification and configurable guardrails policies",
5
- "main": "src/index.ts",
6
- "types": "src/index.ts",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.mts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.mts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./sigv4": {
15
+ "types": "./dist/sigv4.d.mts",
16
+ "import": "./dist/sigv4.mjs",
17
+ "require": "./dist/sigv4.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
7
24
  "scripts": {
8
- "typecheck": "tsc --noEmit"
25
+ "build": "tsdown",
26
+ "typecheck": "tsc --noEmit",
27
+ "prepublishOnly": "npm run build"
9
28
  },
10
29
  "keywords": [
11
30
  "s3",
@@ -13,7 +32,8 @@
13
32
  "sigv4",
14
33
  "aws",
15
34
  "guardrails",
16
- "security"
35
+ "security",
36
+ "cloudflare-workers"
17
37
  ],
18
38
  "license": "MIT",
19
39
  "dependencies": {
@@ -21,8 +41,9 @@
21
41
  "zod": "^4.2.1"
22
42
  },
23
43
  "devDependencies": {
24
- "typescript": "^5.5.2",
25
- "@cloudflare/workers-types": "^4.1.1"
44
+ "@cloudflare/workers-types": "^4.1.1",
45
+ "tsdown": "^0.11.8",
46
+ "typescript": "^5.5.2"
26
47
  },
27
48
  "author": "Tom Shen",
28
49
  "repository": {