@contember/engine-s3-plugin 2.2.0-alpha.3 → 2.2.0-alpha.6

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.
Files changed (68) hide show
  1. package/dist/development/src/Config.cjs.map +1 -1
  2. package/dist/development/src/Config.js.map +1 -1
  3. package/dist/development/src/S3ObjectAuthorizator.cjs +12 -1
  4. package/dist/development/src/S3ObjectAuthorizator.cjs.map +1 -1
  5. package/dist/development/src/S3ObjectAuthorizator.js +12 -1
  6. package/dist/development/src/S3ObjectAuthorizator.js.map +1 -1
  7. package/dist/development/src/S3SchemaContributor.cjs +27 -3
  8. package/dist/development/src/S3SchemaContributor.cjs.map +1 -1
  9. package/dist/development/src/S3SchemaContributor.js +28 -4
  10. package/dist/development/src/S3SchemaContributor.js.map +1 -1
  11. package/dist/development/src/S3SchemaTypes.cjs +8 -0
  12. package/dist/development/src/S3SchemaTypes.cjs.map +1 -1
  13. package/dist/development/src/S3SchemaTypes.js +8 -0
  14. package/dist/development/src/S3SchemaTypes.js.map +1 -1
  15. package/dist/development/src/S3Service.cjs +44 -14
  16. package/dist/development/src/S3Service.cjs.map +1 -1
  17. package/dist/development/src/S3Service.js +44 -14
  18. package/dist/development/src/S3Service.js.map +1 -1
  19. package/dist/development/src/S3Signer.cjs +6 -1
  20. package/dist/development/src/S3Signer.cjs.map +1 -1
  21. package/dist/development/src/S3Signer.js +6 -1
  22. package/dist/development/src/S3Signer.js.map +1 -1
  23. package/dist/production/src/Config.cjs.map +1 -1
  24. package/dist/production/src/Config.js.map +1 -1
  25. package/dist/production/src/S3ObjectAuthorizator.cjs +12 -1
  26. package/dist/production/src/S3ObjectAuthorizator.cjs.map +1 -1
  27. package/dist/production/src/S3ObjectAuthorizator.js +12 -1
  28. package/dist/production/src/S3ObjectAuthorizator.js.map +1 -1
  29. package/dist/production/src/S3SchemaContributor.cjs +27 -3
  30. package/dist/production/src/S3SchemaContributor.cjs.map +1 -1
  31. package/dist/production/src/S3SchemaContributor.js +28 -4
  32. package/dist/production/src/S3SchemaContributor.js.map +1 -1
  33. package/dist/production/src/S3SchemaTypes.cjs +8 -0
  34. package/dist/production/src/S3SchemaTypes.cjs.map +1 -1
  35. package/dist/production/src/S3SchemaTypes.js +8 -0
  36. package/dist/production/src/S3SchemaTypes.js.map +1 -1
  37. package/dist/production/src/S3Service.cjs +44 -14
  38. package/dist/production/src/S3Service.cjs.map +1 -1
  39. package/dist/production/src/S3Service.js +44 -14
  40. package/dist/production/src/S3Service.js.map +1 -1
  41. package/dist/production/src/S3Signer.cjs +6 -1
  42. package/dist/production/src/S3Signer.cjs.map +1 -1
  43. package/dist/production/src/S3Signer.js +6 -1
  44. package/dist/production/src/S3Signer.js.map +1 -1
  45. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  46. package/dist/types/Config.d.ts.map +1 -1
  47. package/dist/types/S3ConfigProcessor.d.ts.map +1 -1
  48. package/dist/types/S3ObjectAuthorizator.d.ts +9 -1
  49. package/dist/types/S3ObjectAuthorizator.d.ts.map +1 -1
  50. package/dist/types/S3SchemaContributor.d.ts +2 -0
  51. package/dist/types/S3SchemaContributor.d.ts.map +1 -1
  52. package/dist/types/S3SchemaTypes.d.ts +1 -0
  53. package/dist/types/S3SchemaTypes.d.ts.map +1 -1
  54. package/dist/types/S3Service.d.ts +22 -1
  55. package/dist/types/S3Service.d.ts.map +1 -1
  56. package/dist/types/S3Signer.d.ts +1 -1
  57. package/dist/types/S3Signer.d.ts.map +1 -1
  58. package/dist/types/index.d.ts.map +1 -1
  59. package/dist/types/tsconfig.tsbuildinfo +1 -1
  60. package/package.json +4 -4
  61. package/src/Config.ts +1 -0
  62. package/src/S3ObjectAuthorizator.ts +20 -0
  63. package/src/S3SchemaContributor.ts +29 -3
  64. package/src/S3SchemaTypes.ts +8 -0
  65. package/src/S3Service.ts +64 -14
  66. package/src/S3Signer.ts +8 -2
  67. package/tests/cases/authorizator.test.ts +11 -0
  68. package/tests/cases/signRequest.test.ts +81 -3
@@ -1 +1 @@
1
- {"version":3,"file":"S3SchemaTypes.cjs","sources":["../../../../packages/engine-s3-plugin/src/S3SchemaTypes.ts"],"sourcesContent":["import { GraphQLEnumType, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'\n\nexport const S3Header = new GraphQLObjectType({\n\tname: 'S3Header',\n\tfields: {\n\t\tkey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tvalue: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3ContentDisposition = new GraphQLEnumType({\n\tname: 'S33ContentDisposition',\n\tvalues: {\n\t\tATTACHMENT: {},\n\t\tINLINE: {},\n\t},\n})\nexport type S3ContentDisposition = 'ATTACHMENT' | 'INLINE'\n\nexport const S3SignedRead = new GraphQLObjectType({\n\tname: 'S3SignedRead',\n\tfields: {\n\t\turl: { type: new GraphQLNonNull(GraphQLString) },\n\t\theaders: {\n\t\t\ttype: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(S3Header))),\n\t\t},\n\t\tmethod: { type: new GraphQLNonNull(GraphQLString) },\n\t\tobjectKey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tbucket: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3SignedUpload = new GraphQLObjectType({\n\tname: 'S3SignedUpload',\n\tfields: {\n\t\turl: { type: new GraphQLNonNull(GraphQLString) },\n\t\theaders: {\n\t\t\ttype: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(S3Header))),\n\t\t},\n\t\tmethod: { type: new GraphQLNonNull(GraphQLString) },\n\t\tobjectKey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tbucket: { type: new GraphQLNonNull(GraphQLString) },\n\t\tpublicUrl: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3Acl = new GraphQLEnumType({\n\tname: 'S3Acl',\n\tvalues: {\n\t\tPUBLIC_READ: {},\n\t\tPRIVATE: {},\n\t\tNONE: {},\n\t},\n})\n\nexport type S3Acl = 'PUBLIC_READ' | 'PRIVATE' | 'NONE'\n\nexport const S3GenerateSignedUploadInput = new GraphQLInputObjectType({\n\tname: 'S3GenerateSignedUploadInput',\n\tfields: {\n\t\tcontentType: { type: GraphQLString },\n\t\textension: { type: GraphQLString, description: 'If not provided, extension is detected from a content-type.' },\n\t\texpiration: { type: GraphQLInt, description: 'Signed URL expiration.' },\n\t\tsize: { type: GraphQLInt, description: 'Uploaded file size. Required when you enable ACL size limits.' },\n\t\tprefix: { type: GraphQLString, description: 'Can be used as a \"directory\".' },\n\t\tsuffix: { type: GraphQLString, description: 'Suffix after generated id and before the extension.' },\n\t\tfileName: { type: GraphQLString, description: 'This only affects Content-disposition header. Does not affect actual object key.' },\n\t\tcontentDisposition: { type: S3ContentDisposition },\n\t\tacl: { type: S3Acl, description: 'If not supported by S3 provider, an error is thrown.' },\n\t},\n})\nexport interface S3GenerateSignedUploadInput {\n\tcontentType: null | string\n\textension: null | string\n\texpiration: null | number\n\tsize: null | number\n\tprefix: null | string\n\tsuffix: null | string\n\tfileName: null | string\n\tcontentDisposition: null | S3ContentDisposition\n\tacl: null | S3Acl\n}\n"],"names":["GraphQLObjectType","GraphQLNonNull","GraphQLString","GraphQLEnumType","GraphQLList","GraphQLInputObjectType","GraphQLInt"],"mappings":";;;AAEO,MAAM,WAAW,IAAIA,QAAAA,kBAAkB;AAAA,EAC7C,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,KAAK,EAAE,MAAM,IAAIC,QAAAA,eAAeC,QAAAA,aAAa,EAAA;AAAA,IAC7C,OAAO,EAAE,MAAM,IAAID,QAAAA,eAAeC,QAAAA,aAAa,EAAA;AAAA,EAAE;AAEnD,CAAC;AAEM,MAAM,uBAAuB,IAAIC,QAAAA,gBAAgB;AAAA,EACvD,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,YAAY,CAAA;AAAA,IACZ,QAAQ,CAAA;AAAA,EAAC;AAEX,CAAC;AAGM,MAAM,eAAe,IAAIH,QAAAA,kBAAkB;AAAA,EACjD,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,KAAK,EAAE,MAAM,IAAIC,QAAAA,eAAeC,QAAAA,aAAa,EAAA;AAAA,IAC7C,SAAS;AAAA,MACR,MAAM,IAAID,QAAAA,eAAe,IAAIG,QAAAA,YAAY,IAAIH,QAAAA,eAAe,QAAQ,CAAC,CAAC;AAAA,IAAA;AAAA,IAEvE,QAAQ,EAAE,MAAM,IAAIA,QAAAA,eAAeC,QAAAA,aAAa,EAAA;AAAA,IAChD,WAAW,EAAE,MAAM,IAAID,QAAAA,eAAeC,QAAAA,aAAa,EAAA;AAAA,IACnD,QAAQ,EAAE,MAAM,IAAID,QAAAA,eAAeC,QAAAA,aAAa,EAAA;AAAA,EAAE;AAEpD,CAAC;AAEM,MAAM,iBAAiB,IAAIF,QAAAA,kBAAkB;AAAA,EACnD,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,KAAK,EAAE,MAAM,IAAIC,QAAAA,eAAeC,QAAAA,aAAa,EAAA;AAAA,IAC7C,SAAS;AAAA,MACR,MAAM,IAAID,QAAAA,eAAe,IAAIG,QAAAA,YAAY,IAAIH,QAAAA,eAAe,QAAQ,CAAC,CAAC;AAAA,IAAA;AAAA,IAEvE,QAAQ,EAAE,MAAM,IAAIA,QAAAA,eAAeC,QAAAA,aAAa,EAAA;AAAA,IAChD,WAAW,EAAE,MAAM,IAAID,QAAAA,eAAeC,QAAAA,aAAa,EAAA;AAAA,IACnD,QAAQ,EAAE,MAAM,IAAID,QAAAA,eAAeC,QAAAA,aAAa,EAAA;AAAA,IAChD,WAAW,EAAE,MAAM,IAAID,QAAAA,eAAeC,QAAAA,aAAa,EAAA;AAAA,EAAE;AAEvD,CAAC;AAEM,MAAM,QAAQ,IAAIC,QAAAA,gBAAgB;AAAA,EACxC,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,aAAa,CAAA;AAAA,IACb,SAAS,CAAA;AAAA,IACT,MAAM,CAAA;AAAA,EAAC;AAET,CAAC;AAIM,MAAM,8BAA8B,IAAIE,QAAAA,uBAAuB;AAAA,EACrE,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,aAAa,EAAE,MAAMH,sBAAA;AAAA,IACrB,WAAW,EAAE,MAAMA,uBAAe,aAAa,8DAAA;AAAA,IAC/C,YAAY,EAAE,MAAMI,oBAAY,aAAa,yBAAA;AAAA,IAC7C,MAAM,EAAE,MAAMA,oBAAY,aAAa,gEAAA;AAAA,IACvC,QAAQ,EAAE,MAAMJ,uBAAe,aAAa,gCAAA;AAAA,IAC5C,QAAQ,EAAE,MAAMA,uBAAe,aAAa,sDAAA;AAAA,IAC5C,UAAU,EAAE,MAAMA,uBAAe,aAAa,mFAAA;AAAA,IAC9C,oBAAoB,EAAE,MAAM,qBAAA;AAAA,IAC5B,KAAK,EAAE,MAAM,OAAO,aAAa,uDAAA;AAAA,EAAuD;AAE1F,CAAC;;;;;;;"}
1
+ {"version":3,"file":"S3SchemaTypes.cjs","sources":["../../../../packages/engine-s3-plugin/src/S3SchemaTypes.ts"],"sourcesContent":["import { GraphQLEnumType, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'\n\nexport const S3Header = new GraphQLObjectType({\n\tname: 'S3Header',\n\tfields: {\n\t\tkey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tvalue: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3ContentDisposition = new GraphQLEnumType({\n\tname: 'S33ContentDisposition',\n\tvalues: {\n\t\tATTACHMENT: {},\n\t\tINLINE: {},\n\t},\n})\nexport type S3ContentDisposition = 'ATTACHMENT' | 'INLINE'\n\nexport const S3SignedRead = new GraphQLObjectType({\n\tname: 'S3SignedRead',\n\tfields: {\n\t\turl: { type: new GraphQLNonNull(GraphQLString) },\n\t\theaders: {\n\t\t\ttype: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(S3Header))),\n\t\t},\n\t\tmethod: { type: new GraphQLNonNull(GraphQLString) },\n\t\tobjectKey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tbucket: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3SignedUpload = new GraphQLObjectType({\n\tname: 'S3SignedUpload',\n\tfields: {\n\t\turl: { type: new GraphQLNonNull(GraphQLString) },\n\t\theaders: {\n\t\t\ttype: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(S3Header))),\n\t\t},\n\t\tmethod: { type: new GraphQLNonNull(GraphQLString) },\n\t\tobjectKey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tbucket: { type: new GraphQLNonNull(GraphQLString) },\n\t\tpublicUrl: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3DeletedObject = new GraphQLObjectType({\n\tname: 'S3DeletedObject',\n\tfields: {\n\t\tobjectKey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tbucket: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3Acl = new GraphQLEnumType({\n\tname: 'S3Acl',\n\tvalues: {\n\t\tPUBLIC_READ: {},\n\t\tPRIVATE: {},\n\t\tNONE: {},\n\t},\n})\n\nexport type S3Acl = 'PUBLIC_READ' | 'PRIVATE' | 'NONE'\n\nexport const S3GenerateSignedUploadInput = new GraphQLInputObjectType({\n\tname: 'S3GenerateSignedUploadInput',\n\tfields: {\n\t\tcontentType: { type: GraphQLString },\n\t\textension: { type: GraphQLString, description: 'If not provided, extension is detected from a content-type.' },\n\t\texpiration: { type: GraphQLInt, description: 'Signed URL expiration.' },\n\t\tsize: { type: GraphQLInt, description: 'Uploaded file size. Required when you enable ACL size limits.' },\n\t\tprefix: { type: GraphQLString, description: 'Can be used as a \"directory\".' },\n\t\tsuffix: { type: GraphQLString, description: 'Suffix after generated id and before the extension.' },\n\t\tfileName: { type: GraphQLString, description: 'This only affects Content-disposition header. Does not affect actual object key.' },\n\t\tcontentDisposition: { type: S3ContentDisposition },\n\t\tacl: { type: S3Acl, description: 'If not supported by S3 provider, an error is thrown.' },\n\t},\n})\nexport interface S3GenerateSignedUploadInput {\n\tcontentType: null | string\n\textension: null | string\n\texpiration: null | number\n\tsize: null | number\n\tprefix: null | string\n\tsuffix: null | string\n\tfileName: null | string\n\tcontentDisposition: null | S3ContentDisposition\n\tacl: null | S3Acl\n}\n"],"names":["GraphQLObjectType","GraphQLNonNull","GraphQLString","GraphQLEnumType","GraphQLList","GraphQLInputObjectType","GraphQLInt"],"mappings":"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"}
@@ -38,6 +38,13 @@ const S3SignedUpload = new GraphQLObjectType({
38
38
  publicUrl: { type: new GraphQLNonNull(GraphQLString) }
39
39
  }
40
40
  });
41
+ const S3DeletedObject = new GraphQLObjectType({
42
+ name: "S3DeletedObject",
43
+ fields: {
44
+ objectKey: { type: new GraphQLNonNull(GraphQLString) },
45
+ bucket: { type: new GraphQLNonNull(GraphQLString) }
46
+ }
47
+ });
41
48
  const S3Acl = new GraphQLEnumType({
42
49
  name: "S3Acl",
43
50
  values: {
@@ -63,6 +70,7 @@ const S3GenerateSignedUploadInput = new GraphQLInputObjectType({
63
70
  export {
64
71
  S3Acl,
65
72
  S3ContentDisposition,
73
+ S3DeletedObject,
66
74
  S3GenerateSignedUploadInput,
67
75
  S3Header,
68
76
  S3SignedRead,
@@ -1 +1 @@
1
- {"version":3,"file":"S3SchemaTypes.js","sources":["../../../../packages/engine-s3-plugin/src/S3SchemaTypes.ts"],"sourcesContent":["import { GraphQLEnumType, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'\n\nexport const S3Header = new GraphQLObjectType({\n\tname: 'S3Header',\n\tfields: {\n\t\tkey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tvalue: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3ContentDisposition = new GraphQLEnumType({\n\tname: 'S33ContentDisposition',\n\tvalues: {\n\t\tATTACHMENT: {},\n\t\tINLINE: {},\n\t},\n})\nexport type S3ContentDisposition = 'ATTACHMENT' | 'INLINE'\n\nexport const S3SignedRead = new GraphQLObjectType({\n\tname: 'S3SignedRead',\n\tfields: {\n\t\turl: { type: new GraphQLNonNull(GraphQLString) },\n\t\theaders: {\n\t\t\ttype: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(S3Header))),\n\t\t},\n\t\tmethod: { type: new GraphQLNonNull(GraphQLString) },\n\t\tobjectKey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tbucket: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3SignedUpload = new GraphQLObjectType({\n\tname: 'S3SignedUpload',\n\tfields: {\n\t\turl: { type: new GraphQLNonNull(GraphQLString) },\n\t\theaders: {\n\t\t\ttype: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(S3Header))),\n\t\t},\n\t\tmethod: { type: new GraphQLNonNull(GraphQLString) },\n\t\tobjectKey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tbucket: { type: new GraphQLNonNull(GraphQLString) },\n\t\tpublicUrl: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3Acl = new GraphQLEnumType({\n\tname: 'S3Acl',\n\tvalues: {\n\t\tPUBLIC_READ: {},\n\t\tPRIVATE: {},\n\t\tNONE: {},\n\t},\n})\n\nexport type S3Acl = 'PUBLIC_READ' | 'PRIVATE' | 'NONE'\n\nexport const S3GenerateSignedUploadInput = new GraphQLInputObjectType({\n\tname: 'S3GenerateSignedUploadInput',\n\tfields: {\n\t\tcontentType: { type: GraphQLString },\n\t\textension: { type: GraphQLString, description: 'If not provided, extension is detected from a content-type.' },\n\t\texpiration: { type: GraphQLInt, description: 'Signed URL expiration.' },\n\t\tsize: { type: GraphQLInt, description: 'Uploaded file size. Required when you enable ACL size limits.' },\n\t\tprefix: { type: GraphQLString, description: 'Can be used as a \"directory\".' },\n\t\tsuffix: { type: GraphQLString, description: 'Suffix after generated id and before the extension.' },\n\t\tfileName: { type: GraphQLString, description: 'This only affects Content-disposition header. Does not affect actual object key.' },\n\t\tcontentDisposition: { type: S3ContentDisposition },\n\t\tacl: { type: S3Acl, description: 'If not supported by S3 provider, an error is thrown.' },\n\t},\n})\nexport interface S3GenerateSignedUploadInput {\n\tcontentType: null | string\n\textension: null | string\n\texpiration: null | number\n\tsize: null | number\n\tprefix: null | string\n\tsuffix: null | string\n\tfileName: null | string\n\tcontentDisposition: null | S3ContentDisposition\n\tacl: null | S3Acl\n}\n"],"names":[],"mappings":";AAEO,MAAM,WAAW,IAAI,kBAAkB;AAAA,EAC7C,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,KAAK,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IAC7C,OAAO,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,EAAE;AAEnD,CAAC;AAEM,MAAM,uBAAuB,IAAI,gBAAgB;AAAA,EACvD,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,YAAY,CAAA;AAAA,IACZ,QAAQ,CAAA;AAAA,EAAC;AAEX,CAAC;AAGM,MAAM,eAAe,IAAI,kBAAkB;AAAA,EACjD,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,KAAK,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IAC7C,SAAS;AAAA,MACR,MAAM,IAAI,eAAe,IAAI,YAAY,IAAI,eAAe,QAAQ,CAAC,CAAC;AAAA,IAAA;AAAA,IAEvE,QAAQ,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IAChD,WAAW,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IACnD,QAAQ,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,EAAE;AAEpD,CAAC;AAEM,MAAM,iBAAiB,IAAI,kBAAkB;AAAA,EACnD,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,KAAK,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IAC7C,SAAS;AAAA,MACR,MAAM,IAAI,eAAe,IAAI,YAAY,IAAI,eAAe,QAAQ,CAAC,CAAC;AAAA,IAAA;AAAA,IAEvE,QAAQ,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IAChD,WAAW,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IACnD,QAAQ,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IAChD,WAAW,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,EAAE;AAEvD,CAAC;AAEM,MAAM,QAAQ,IAAI,gBAAgB;AAAA,EACxC,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,aAAa,CAAA;AAAA,IACb,SAAS,CAAA;AAAA,IACT,MAAM,CAAA;AAAA,EAAC;AAET,CAAC;AAIM,MAAM,8BAA8B,IAAI,uBAAuB;AAAA,EACrE,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,aAAa,EAAE,MAAM,cAAA;AAAA,IACrB,WAAW,EAAE,MAAM,eAAe,aAAa,8DAAA;AAAA,IAC/C,YAAY,EAAE,MAAM,YAAY,aAAa,yBAAA;AAAA,IAC7C,MAAM,EAAE,MAAM,YAAY,aAAa,gEAAA;AAAA,IACvC,QAAQ,EAAE,MAAM,eAAe,aAAa,gCAAA;AAAA,IAC5C,QAAQ,EAAE,MAAM,eAAe,aAAa,sDAAA;AAAA,IAC5C,UAAU,EAAE,MAAM,eAAe,aAAa,mFAAA;AAAA,IAC9C,oBAAoB,EAAE,MAAM,qBAAA;AAAA,IAC5B,KAAK,EAAE,MAAM,OAAO,aAAa,uDAAA;AAAA,EAAuD;AAE1F,CAAC;"}
1
+ {"version":3,"file":"S3SchemaTypes.js","sources":["../../../../packages/engine-s3-plugin/src/S3SchemaTypes.ts"],"sourcesContent":["import { GraphQLEnumType, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'\n\nexport const S3Header = new GraphQLObjectType({\n\tname: 'S3Header',\n\tfields: {\n\t\tkey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tvalue: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3ContentDisposition = new GraphQLEnumType({\n\tname: 'S33ContentDisposition',\n\tvalues: {\n\t\tATTACHMENT: {},\n\t\tINLINE: {},\n\t},\n})\nexport type S3ContentDisposition = 'ATTACHMENT' | 'INLINE'\n\nexport const S3SignedRead = new GraphQLObjectType({\n\tname: 'S3SignedRead',\n\tfields: {\n\t\turl: { type: new GraphQLNonNull(GraphQLString) },\n\t\theaders: {\n\t\t\ttype: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(S3Header))),\n\t\t},\n\t\tmethod: { type: new GraphQLNonNull(GraphQLString) },\n\t\tobjectKey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tbucket: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3SignedUpload = new GraphQLObjectType({\n\tname: 'S3SignedUpload',\n\tfields: {\n\t\turl: { type: new GraphQLNonNull(GraphQLString) },\n\t\theaders: {\n\t\t\ttype: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(S3Header))),\n\t\t},\n\t\tmethod: { type: new GraphQLNonNull(GraphQLString) },\n\t\tobjectKey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tbucket: { type: new GraphQLNonNull(GraphQLString) },\n\t\tpublicUrl: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3DeletedObject = new GraphQLObjectType({\n\tname: 'S3DeletedObject',\n\tfields: {\n\t\tobjectKey: { type: new GraphQLNonNull(GraphQLString) },\n\t\tbucket: { type: new GraphQLNonNull(GraphQLString) },\n\t},\n})\n\nexport const S3Acl = new GraphQLEnumType({\n\tname: 'S3Acl',\n\tvalues: {\n\t\tPUBLIC_READ: {},\n\t\tPRIVATE: {},\n\t\tNONE: {},\n\t},\n})\n\nexport type S3Acl = 'PUBLIC_READ' | 'PRIVATE' | 'NONE'\n\nexport const S3GenerateSignedUploadInput = new GraphQLInputObjectType({\n\tname: 'S3GenerateSignedUploadInput',\n\tfields: {\n\t\tcontentType: { type: GraphQLString },\n\t\textension: { type: GraphQLString, description: 'If not provided, extension is detected from a content-type.' },\n\t\texpiration: { type: GraphQLInt, description: 'Signed URL expiration.' },\n\t\tsize: { type: GraphQLInt, description: 'Uploaded file size. Required when you enable ACL size limits.' },\n\t\tprefix: { type: GraphQLString, description: 'Can be used as a \"directory\".' },\n\t\tsuffix: { type: GraphQLString, description: 'Suffix after generated id and before the extension.' },\n\t\tfileName: { type: GraphQLString, description: 'This only affects Content-disposition header. Does not affect actual object key.' },\n\t\tcontentDisposition: { type: S3ContentDisposition },\n\t\tacl: { type: S3Acl, description: 'If not supported by S3 provider, an error is thrown.' },\n\t},\n})\nexport interface S3GenerateSignedUploadInput {\n\tcontentType: null | string\n\textension: null | string\n\texpiration: null | number\n\tsize: null | number\n\tprefix: null | string\n\tsuffix: null | string\n\tfileName: null | string\n\tcontentDisposition: null | S3ContentDisposition\n\tacl: null | S3Acl\n}\n"],"names":[],"mappings":";AAEO,MAAM,WAAW,IAAI,kBAAkB;AAAA,EAC7C,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,KAAK,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IAC7C,OAAO,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,EAAE;AAEnD,CAAC;AAEM,MAAM,uBAAuB,IAAI,gBAAgB;AAAA,EACvD,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,YAAY,CAAA;AAAA,IACZ,QAAQ,CAAA;AAAA,EAAC;AAEX,CAAC;AAGM,MAAM,eAAe,IAAI,kBAAkB;AAAA,EACjD,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,KAAK,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IAC7C,SAAS;AAAA,MACR,MAAM,IAAI,eAAe,IAAI,YAAY,IAAI,eAAe,QAAQ,CAAC,CAAC;AAAA,IAAA;AAAA,IAEvE,QAAQ,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IAChD,WAAW,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IACnD,QAAQ,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,EAAE;AAEpD,CAAC;AAEM,MAAM,iBAAiB,IAAI,kBAAkB;AAAA,EACnD,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,KAAK,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IAC7C,SAAS;AAAA,MACR,MAAM,IAAI,eAAe,IAAI,YAAY,IAAI,eAAe,QAAQ,CAAC,CAAC;AAAA,IAAA;AAAA,IAEvE,QAAQ,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IAChD,WAAW,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IACnD,QAAQ,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IAChD,WAAW,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,EAAE;AAEvD,CAAC;AAEM,MAAM,kBAAkB,IAAI,kBAAkB;AAAA,EACpD,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,WAAW,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,IACnD,QAAQ,EAAE,MAAM,IAAI,eAAe,aAAa,EAAA;AAAA,EAAE;AAEpD,CAAC;AAEM,MAAM,QAAQ,IAAI,gBAAgB;AAAA,EACxC,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,aAAa,CAAA;AAAA,IACb,SAAS,CAAA;AAAA,IACT,MAAM,CAAA;AAAA,EAAC;AAET,CAAC;AAIM,MAAM,8BAA8B,IAAI,uBAAuB;AAAA,EACrE,MAAM;AAAA,EACN,QAAQ;AAAA,IACP,aAAa,EAAE,MAAM,cAAA;AAAA,IACrB,WAAW,EAAE,MAAM,eAAe,aAAa,8DAAA;AAAA,IAC/C,YAAY,EAAE,MAAM,YAAY,aAAa,yBAAA;AAAA,IAC7C,MAAM,EAAE,MAAM,YAAY,aAAa,gEAAA;AAAA,IACvC,QAAQ,EAAE,MAAM,eAAe,aAAa,gCAAA;AAAA,IAC5C,QAAQ,EAAE,MAAM,eAAe,aAAa,sDAAA;AAAA,IAC5C,UAAU,EAAE,MAAM,eAAe,aAAa,mFAAA;AAAA,IAC9C,oBAAoB,EAAE,MAAM,qBAAA;AAAA,IAC5B,KAAK,EAAE,MAAM,OAAO,aAAa,uDAAA;AAAA,EAAuD;AAE1F,CAAC;"}
@@ -7,11 +7,14 @@ const S3Signer = require("./S3Signer.cjs");
7
7
  var __defProp = Object.defineProperty;
8
8
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
9
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
10
+ const deleteUrlExpiration = 60;
11
+ const deleteTimeoutMs = 1e4;
10
12
  class S3Service {
11
- constructor(config, providers, authorizator) {
13
+ constructor(config, providers, authorizator, fetch = (url, init) => globalThis.fetch(url, init)) {
12
14
  this.config = config;
13
15
  this.providers = providers;
14
16
  this.authorizator = authorizator;
17
+ this.fetch = fetch;
15
18
  __publicField(this, "publicBaseUrl");
16
19
  __publicField(this, "signer");
17
20
  this.publicBaseUrl = Config.resolveS3PublicBaseUrl(config);
@@ -66,34 +69,61 @@ class S3Service {
66
69
  }
67
70
  getSignedReadUrl({ objectKey, expiration }) {
68
71
  const bucket = this.config.bucket;
69
- const publicPrefix = this.formatPublicUrl("");
70
- if (objectKey.startsWith(publicPrefix)) {
71
- objectKey = objectKey.substring(publicPrefix.length);
72
- }
73
- if (this.config.prefix && !objectKey.startsWith(this.config.prefix)) {
74
- throw new graphqlUtils.ForbiddenError(
75
- `Given object key "${objectKey}" does not start with a project prefix "${this.config.prefix}"`
76
- );
77
- }
78
- const localObjectKey = this.config.prefix ? objectKey.substring(this.config.prefix.length + 1) : objectKey;
79
- this.authorizator.verifyReadAccess({ key: localObjectKey });
72
+ const resolved = this.resolveObjectKey(objectKey);
73
+ this.authorizator.verifyReadAccess({ key: resolved.localObjectKey });
80
74
  const url = this.signer.sign({
81
75
  action: "read",
82
76
  expiration: expiration ?? 3600,
83
- key: objectKey,
77
+ key: resolved.objectKey,
84
78
  headers: {}
85
79
  });
86
80
  return {
87
81
  bucket,
88
- objectKey,
82
+ objectKey: resolved.objectKey,
89
83
  url,
90
84
  headers: [],
91
85
  method: "GET"
92
86
  };
93
87
  }
88
+ /**
89
+ * Unlike an upload or a read, a delete carries no payload, so the signed URL never leaves the server.
90
+ */
91
+ async deleteObject({ objectKey }) {
92
+ const bucket = this.config.bucket;
93
+ const resolved = this.resolveObjectKey(objectKey);
94
+ this.authorizator.verifyDeleteAccess({ key: resolved.localObjectKey });
95
+ const url = this.signer.sign({
96
+ action: "delete",
97
+ expiration: deleteUrlExpiration,
98
+ key: resolved.objectKey,
99
+ headers: {}
100
+ });
101
+ const response = await this.fetch(url, { method: "DELETE", signal: AbortSignal.timeout(deleteTimeoutMs) });
102
+ if (!response.ok) {
103
+ throw new Error(`Failed to delete an object "${resolved.objectKey}": S3 responded with ${response.status}: ${await response.text()}`);
104
+ }
105
+ return {
106
+ bucket,
107
+ objectKey: resolved.objectKey
108
+ };
109
+ }
94
110
  formatPublicUrl(key) {
95
111
  return `${this.publicBaseUrl}/${key}`;
96
112
  }
113
+ resolveObjectKey(objectKey) {
114
+ const publicPrefix = this.formatPublicUrl("");
115
+ if (objectKey.startsWith(publicPrefix)) {
116
+ objectKey = objectKey.substring(publicPrefix.length);
117
+ }
118
+ const prefix = this.config.prefix ? this.config.prefix + "/" : "";
119
+ if (prefix && !objectKey.startsWith(prefix)) {
120
+ throw new graphqlUtils.ForbiddenError(
121
+ `Given object key "${objectKey}" does not start with a project prefix "${this.config.prefix}"`
122
+ );
123
+ }
124
+ const localObjectKey = objectKey.substring(prefix.length);
125
+ return { objectKey, localObjectKey };
126
+ }
97
127
  }
98
128
  class S3ServiceFactory {
99
129
  create(config, providers, authorizator) {
@@ -1 +1 @@
1
- {"version":3,"file":"S3Service.cjs","sources":["../../../../packages/engine-s3-plugin/src/S3Service.ts"],"sourcesContent":["import { extension as resolveExtension } from 'mime-types'\nimport { resolveS3PublicBaseUrl, S3Config } from './Config.js'\nimport { ForbiddenError, UserInputError } from '@contember/graphql-utils'\nimport { Providers } from '@contember/engine-plugins'\nimport { S3Signer } from './S3Signer.js'\nimport { S3GenerateSignedUploadInput } from './S3SchemaTypes.js'\nimport { S3ObjectAuthorizator } from './S3ObjectAuthorizator.js'\n\ntype SignedReadUrl = {\n\turl: string\n\tbucket: string\n\tobjectKey: string\n\theaders: { key: string; value: string }[]\n\tmethod: string\n}\n\ntype SignedUploadUrl = {\n\tobjectKey: string\n\turl: string\n\tbucket: string\n\tpublicUrl: string\n\theaders: { key: string; value: string }[]\n\tmethod: string\n}\n\nexport class S3Service {\n\tprivate readonly publicBaseUrl: string\n\n\tprivate readonly signer: S3Signer\n\n\tconstructor(\n\t\tpublic readonly config: S3Config,\n\t\tprivate readonly providers: Pick<Providers, 'uuid' | 'now'>,\n\t\tprivate readonly authorizator: S3ObjectAuthorizator,\n\t) {\n\t\tthis.publicBaseUrl = resolveS3PublicBaseUrl(config)\n\t\tthis.signer = new S3Signer(config, providers)\n\t}\n\n\tpublic getSignedUploadUrl(\n\t\t{ acl, contentDisposition, contentType, expiration, prefix, suffix, fileName, extension, size }: S3GenerateSignedUploadInput,\n\t): SignedUploadUrl {\n\t\tconst ext = extension ?? ((contentType ? resolveExtension(contentType) : null) || 'bin')\n\t\tconst id = this.providers.uuid()\n\t\tconst localObjectKey = (prefix ? prefix + '/' : '') + `${id}${suffix ?? ''}.${ext}`\n\t\tthis.authorizator.verifyUploadAccess({ key: localObjectKey, size })\n\n\t\tconst objectKey = (this.config.prefix ? this.config.prefix + '/' : '') + localObjectKey\n\n\t\tconst bucket = this.config.bucket\n\t\tif (acl && this.config.noAcl) {\n\t\t\tthrow new UserInputError('ACL is not supported')\n\t\t}\n\n\t\tconst headers: Record<string, string> = {\n\t\t\t'Cache-Control': 'immutable',\n\t\t}\n\t\tif (contentType) {\n\t\t\theaders['Content-Type'] = contentType\n\t\t}\n\n\t\tif (!this.config.noAcl && acl !== 'NONE') {\n\t\t\tconst mapping = { PUBLIC_READ: 'public-read', PRIVATE: 'private' }\n\t\t\theaders['x-amz-acl'] = mapping[acl ?? 'PUBLIC_READ']\n\t\t}\n\t\tif (fileName || contentDisposition) {\n\t\t\tlet contentDispositionHeader = contentDisposition?.toLowerCase() ?? 'inline'\n\t\t\tif (fileName) {\n\t\t\t\tcontentDispositionHeader += `; filename*=UTF-8''${encodeURIComponent(fileName)}`\n\t\t\t}\n\t\t\theaders['Content-Disposition'] = contentDispositionHeader\n\t\t}\n\t\tconst headersToSend = Object.entries(headers).map(([key, value]) => ({ key, value }))\n\n\t\t// intentionally not sending following headers\n\t\tif (size) {\n\t\t\theaders['Content-Length'] = String(size)\n\t\t}\n\n\t\tconst url = this.signer.sign({\n\t\t\taction: 'upload',\n\t\t\texpiration: expiration ?? 3600,\n\t\t\tkey: objectKey,\n\t\t\theaders: { ...headers },\n\t\t})\n\n\t\tconst publicUrl = this.formatPublicUrl(objectKey)\n\t\treturn {\n\t\t\tbucket,\n\t\t\tobjectKey,\n\t\t\turl,\n\t\t\tpublicUrl,\n\t\t\theaders: headersToSend,\n\t\t\tmethod: 'PUT',\n\t\t}\n\t}\n\n\tpublic getSignedReadUrl({ objectKey, expiration }: { objectKey: string; expiration: number | null }): SignedReadUrl {\n\t\tconst bucket = this.config.bucket\n\n\t\tconst publicPrefix = this.formatPublicUrl('')\n\t\tif (objectKey.startsWith(publicPrefix)) {\n\t\t\tobjectKey = objectKey.substring(publicPrefix.length)\n\t\t}\n\t\tif (this.config.prefix && !objectKey.startsWith(this.config.prefix)) {\n\t\t\tthrow new ForbiddenError(\n\t\t\t\t`Given object key \"${objectKey}\" does not start with a project prefix \"${this.config.prefix}\"`,\n\t\t\t)\n\t\t}\n\t\tconst localObjectKey = this.config.prefix ? objectKey.substring(this.config.prefix.length + 1) : objectKey\n\t\tthis.authorizator.verifyReadAccess({ key: localObjectKey })\n\n\t\tconst url = this.signer.sign({\n\t\t\taction: 'read',\n\t\t\texpiration: expiration ?? 3600,\n\t\t\tkey: objectKey,\n\t\t\theaders: {},\n\t\t})\n\n\t\treturn {\n\t\t\tbucket,\n\t\t\tobjectKey,\n\t\t\turl,\n\t\t\theaders: [],\n\t\t\tmethod: 'GET',\n\t\t}\n\t}\n\n\tpublic formatPublicUrl(key: string): string {\n\t\treturn `${this.publicBaseUrl}/${key}`\n\t}\n}\n\nexport class S3ServiceFactory {\n\tpublic create(config: S3Config, providers: Providers, authorizator: S3ObjectAuthorizator) {\n\t\treturn new S3Service(config, providers, authorizator)\n\t}\n}\n"],"names":["resolveS3PublicBaseUrl","S3Signer","resolveExtension","UserInputError","ForbiddenError"],"mappings":"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"}
1
+ {"version":3,"file":"S3Service.cjs","sources":["../../../../packages/engine-s3-plugin/src/S3Service.ts"],"sourcesContent":["import { extension as resolveExtension } from 'mime-types'\nimport { resolveS3PublicBaseUrl, S3Config } from './Config.js'\nimport { ForbiddenError, UserInputError } from '@contember/graphql-utils'\nimport { Providers } from '@contember/engine-plugins'\nimport { S3Signer } from './S3Signer.js'\nimport { S3GenerateSignedUploadInput } from './S3SchemaTypes.js'\nimport { S3ObjectAuthorizator } from './S3ObjectAuthorizator.js'\n\ntype SignedReadUrl = {\n\turl: string\n\tbucket: string\n\tobjectKey: string\n\theaders: { key: string; value: string }[]\n\tmethod: string\n}\n\ntype SignedUploadUrl = {\n\tobjectKey: string\n\turl: string\n\tbucket: string\n\tpublicUrl: string\n\theaders: { key: string; value: string }[]\n\tmethod: string\n}\n\ntype DeletedObject = {\n\tbucket: string\n\tobjectKey: string\n}\n\nexport type S3FetchResponse = {\n\treadonly ok: boolean\n\treadonly status: number\n\ttext(): Promise<string>\n}\n\nexport type S3Fetch = (url: string, init: { method: string; signal: AbortSignal }) => Promise<S3FetchResponse>\n\nconst deleteUrlExpiration = 60\nconst deleteTimeoutMs = 10_000\n\nexport class S3Service {\n\tprivate readonly publicBaseUrl: string\n\n\tprivate readonly signer: S3Signer\n\n\tconstructor(\n\t\tpublic readonly config: S3Config,\n\t\tprivate readonly providers: Pick<Providers, 'uuid' | 'now'>,\n\t\tprivate readonly authorizator: S3ObjectAuthorizator,\n\t\tprivate readonly fetch: S3Fetch = (url, init) => globalThis.fetch(url, init),\n\t) {\n\t\tthis.publicBaseUrl = resolveS3PublicBaseUrl(config)\n\t\tthis.signer = new S3Signer(config, providers)\n\t}\n\n\tpublic getSignedUploadUrl(\n\t\t{ acl, contentDisposition, contentType, expiration, prefix, suffix, fileName, extension, size }: S3GenerateSignedUploadInput,\n\t): SignedUploadUrl {\n\t\tconst ext = extension ?? ((contentType ? resolveExtension(contentType) : null) || 'bin')\n\t\tconst id = this.providers.uuid()\n\t\tconst localObjectKey = (prefix ? prefix + '/' : '') + `${id}${suffix ?? ''}.${ext}`\n\t\tthis.authorizator.verifyUploadAccess({ key: localObjectKey, size })\n\n\t\tconst objectKey = (this.config.prefix ? this.config.prefix + '/' : '') + localObjectKey\n\n\t\tconst bucket = this.config.bucket\n\t\tif (acl && this.config.noAcl) {\n\t\t\tthrow new UserInputError('ACL is not supported')\n\t\t}\n\n\t\tconst headers: Record<string, string> = {\n\t\t\t'Cache-Control': 'immutable',\n\t\t}\n\t\tif (contentType) {\n\t\t\theaders['Content-Type'] = contentType\n\t\t}\n\n\t\tif (!this.config.noAcl && acl !== 'NONE') {\n\t\t\tconst mapping = { PUBLIC_READ: 'public-read', PRIVATE: 'private' }\n\t\t\theaders['x-amz-acl'] = mapping[acl ?? 'PUBLIC_READ']\n\t\t}\n\t\tif (fileName || contentDisposition) {\n\t\t\tlet contentDispositionHeader = contentDisposition?.toLowerCase() ?? 'inline'\n\t\t\tif (fileName) {\n\t\t\t\tcontentDispositionHeader += `; filename*=UTF-8''${encodeURIComponent(fileName)}`\n\t\t\t}\n\t\t\theaders['Content-Disposition'] = contentDispositionHeader\n\t\t}\n\t\tconst headersToSend = Object.entries(headers).map(([key, value]) => ({ key, value }))\n\n\t\t// intentionally not sending following headers\n\t\tif (size) {\n\t\t\theaders['Content-Length'] = String(size)\n\t\t}\n\n\t\tconst url = this.signer.sign({\n\t\t\taction: 'upload',\n\t\t\texpiration: expiration ?? 3600,\n\t\t\tkey: objectKey,\n\t\t\theaders: { ...headers },\n\t\t})\n\n\t\tconst publicUrl = this.formatPublicUrl(objectKey)\n\t\treturn {\n\t\t\tbucket,\n\t\t\tobjectKey,\n\t\t\turl,\n\t\t\tpublicUrl,\n\t\t\theaders: headersToSend,\n\t\t\tmethod: 'PUT',\n\t\t}\n\t}\n\n\tpublic getSignedReadUrl({ objectKey, expiration }: { objectKey: string; expiration: number | null }): SignedReadUrl {\n\t\tconst bucket = this.config.bucket\n\t\tconst resolved = this.resolveObjectKey(objectKey)\n\t\tthis.authorizator.verifyReadAccess({ key: resolved.localObjectKey })\n\n\t\tconst url = this.signer.sign({\n\t\t\taction: 'read',\n\t\t\texpiration: expiration ?? 3600,\n\t\t\tkey: resolved.objectKey,\n\t\t\theaders: {},\n\t\t})\n\n\t\treturn {\n\t\t\tbucket,\n\t\t\tobjectKey: resolved.objectKey,\n\t\t\turl,\n\t\t\theaders: [],\n\t\t\tmethod: 'GET',\n\t\t}\n\t}\n\n\t/**\n\t * Unlike an upload or a read, a delete carries no payload, so the signed URL never leaves the server.\n\t */\n\tpublic async deleteObject({ objectKey }: { objectKey: string }): Promise<DeletedObject> {\n\t\tconst bucket = this.config.bucket\n\t\tconst resolved = this.resolveObjectKey(objectKey)\n\t\tthis.authorizator.verifyDeleteAccess({ key: resolved.localObjectKey })\n\n\t\tconst url = this.signer.sign({\n\t\t\taction: 'delete',\n\t\t\texpiration: deleteUrlExpiration,\n\t\t\tkey: resolved.objectKey,\n\t\t\theaders: {},\n\t\t})\n\n\t\tconst response = await this.fetch(url, { method: 'DELETE', signal: AbortSignal.timeout(deleteTimeoutMs) })\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to delete an object \"${resolved.objectKey}\": S3 responded with ${response.status}: ${await response.text()}`)\n\t\t}\n\n\t\treturn {\n\t\t\tbucket,\n\t\t\tobjectKey: resolved.objectKey,\n\t\t}\n\t}\n\n\tpublic formatPublicUrl(key: string): string {\n\t\treturn `${this.publicBaseUrl}/${key}`\n\t}\n\n\tprivate resolveObjectKey(objectKey: string): { objectKey: string; localObjectKey: string } {\n\t\tconst publicPrefix = this.formatPublicUrl('')\n\t\tif (objectKey.startsWith(publicPrefix)) {\n\t\t\tobjectKey = objectKey.substring(publicPrefix.length)\n\t\t}\n\t\t// the separator is a part of the prefix: without it, a project prefixed \"blog\" reaches into \"blog-staging\" in a shared bucket\n\t\tconst prefix = this.config.prefix ? this.config.prefix + '/' : ''\n\t\tif (prefix && !objectKey.startsWith(prefix)) {\n\t\t\tthrow new ForbiddenError(\n\t\t\t\t`Given object key \"${objectKey}\" does not start with a project prefix \"${this.config.prefix}\"`,\n\t\t\t)\n\t\t}\n\t\tconst localObjectKey = objectKey.substring(prefix.length)\n\n\t\treturn { objectKey, localObjectKey }\n\t}\n}\n\nexport class S3ServiceFactory {\n\tpublic create(config: S3Config, providers: Providers, authorizator: S3ObjectAuthorizator) {\n\t\treturn new S3Service(config, providers, authorizator)\n\t}\n}\n"],"names":["resolveS3PublicBaseUrl","S3Signer","resolveExtension","UserInputError","ForbiddenError"],"mappings":"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"}
@@ -5,11 +5,14 @@ import { S3Signer } from "./S3Signer.js";
5
5
  var __defProp = Object.defineProperty;
6
6
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
7
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
8
+ const deleteUrlExpiration = 60;
9
+ const deleteTimeoutMs = 1e4;
8
10
  class S3Service {
9
- constructor(config, providers, authorizator) {
11
+ constructor(config, providers, authorizator, fetch = (url, init) => globalThis.fetch(url, init)) {
10
12
  this.config = config;
11
13
  this.providers = providers;
12
14
  this.authorizator = authorizator;
15
+ this.fetch = fetch;
13
16
  __publicField(this, "publicBaseUrl");
14
17
  __publicField(this, "signer");
15
18
  this.publicBaseUrl = resolveS3PublicBaseUrl(config);
@@ -64,34 +67,61 @@ class S3Service {
64
67
  }
65
68
  getSignedReadUrl({ objectKey, expiration }) {
66
69
  const bucket = this.config.bucket;
67
- const publicPrefix = this.formatPublicUrl("");
68
- if (objectKey.startsWith(publicPrefix)) {
69
- objectKey = objectKey.substring(publicPrefix.length);
70
- }
71
- if (this.config.prefix && !objectKey.startsWith(this.config.prefix)) {
72
- throw new ForbiddenError(
73
- `Given object key "${objectKey}" does not start with a project prefix "${this.config.prefix}"`
74
- );
75
- }
76
- const localObjectKey = this.config.prefix ? objectKey.substring(this.config.prefix.length + 1) : objectKey;
77
- this.authorizator.verifyReadAccess({ key: localObjectKey });
70
+ const resolved = this.resolveObjectKey(objectKey);
71
+ this.authorizator.verifyReadAccess({ key: resolved.localObjectKey });
78
72
  const url = this.signer.sign({
79
73
  action: "read",
80
74
  expiration: expiration ?? 3600,
81
- key: objectKey,
75
+ key: resolved.objectKey,
82
76
  headers: {}
83
77
  });
84
78
  return {
85
79
  bucket,
86
- objectKey,
80
+ objectKey: resolved.objectKey,
87
81
  url,
88
82
  headers: [],
89
83
  method: "GET"
90
84
  };
91
85
  }
86
+ /**
87
+ * Unlike an upload or a read, a delete carries no payload, so the signed URL never leaves the server.
88
+ */
89
+ async deleteObject({ objectKey }) {
90
+ const bucket = this.config.bucket;
91
+ const resolved = this.resolveObjectKey(objectKey);
92
+ this.authorizator.verifyDeleteAccess({ key: resolved.localObjectKey });
93
+ const url = this.signer.sign({
94
+ action: "delete",
95
+ expiration: deleteUrlExpiration,
96
+ key: resolved.objectKey,
97
+ headers: {}
98
+ });
99
+ const response = await this.fetch(url, { method: "DELETE", signal: AbortSignal.timeout(deleteTimeoutMs) });
100
+ if (!response.ok) {
101
+ throw new Error(`Failed to delete an object "${resolved.objectKey}": S3 responded with ${response.status}: ${await response.text()}`);
102
+ }
103
+ return {
104
+ bucket,
105
+ objectKey: resolved.objectKey
106
+ };
107
+ }
92
108
  formatPublicUrl(key) {
93
109
  return `${this.publicBaseUrl}/${key}`;
94
110
  }
111
+ resolveObjectKey(objectKey) {
112
+ const publicPrefix = this.formatPublicUrl("");
113
+ if (objectKey.startsWith(publicPrefix)) {
114
+ objectKey = objectKey.substring(publicPrefix.length);
115
+ }
116
+ const prefix = this.config.prefix ? this.config.prefix + "/" : "";
117
+ if (prefix && !objectKey.startsWith(prefix)) {
118
+ throw new ForbiddenError(
119
+ `Given object key "${objectKey}" does not start with a project prefix "${this.config.prefix}"`
120
+ );
121
+ }
122
+ const localObjectKey = objectKey.substring(prefix.length);
123
+ return { objectKey, localObjectKey };
124
+ }
95
125
  }
96
126
  class S3ServiceFactory {
97
127
  create(config, providers, authorizator) {
@@ -1 +1 @@
1
- {"version":3,"file":"S3Service.js","sources":["../../../../packages/engine-s3-plugin/src/S3Service.ts"],"sourcesContent":["import { extension as resolveExtension } from 'mime-types'\nimport { resolveS3PublicBaseUrl, S3Config } from './Config.js'\nimport { ForbiddenError, UserInputError } from '@contember/graphql-utils'\nimport { Providers } from '@contember/engine-plugins'\nimport { S3Signer } from './S3Signer.js'\nimport { S3GenerateSignedUploadInput } from './S3SchemaTypes.js'\nimport { S3ObjectAuthorizator } from './S3ObjectAuthorizator.js'\n\ntype SignedReadUrl = {\n\turl: string\n\tbucket: string\n\tobjectKey: string\n\theaders: { key: string; value: string }[]\n\tmethod: string\n}\n\ntype SignedUploadUrl = {\n\tobjectKey: string\n\turl: string\n\tbucket: string\n\tpublicUrl: string\n\theaders: { key: string; value: string }[]\n\tmethod: string\n}\n\nexport class S3Service {\n\tprivate readonly publicBaseUrl: string\n\n\tprivate readonly signer: S3Signer\n\n\tconstructor(\n\t\tpublic readonly config: S3Config,\n\t\tprivate readonly providers: Pick<Providers, 'uuid' | 'now'>,\n\t\tprivate readonly authorizator: S3ObjectAuthorizator,\n\t) {\n\t\tthis.publicBaseUrl = resolveS3PublicBaseUrl(config)\n\t\tthis.signer = new S3Signer(config, providers)\n\t}\n\n\tpublic getSignedUploadUrl(\n\t\t{ acl, contentDisposition, contentType, expiration, prefix, suffix, fileName, extension, size }: S3GenerateSignedUploadInput,\n\t): SignedUploadUrl {\n\t\tconst ext = extension ?? ((contentType ? resolveExtension(contentType) : null) || 'bin')\n\t\tconst id = this.providers.uuid()\n\t\tconst localObjectKey = (prefix ? prefix + '/' : '') + `${id}${suffix ?? ''}.${ext}`\n\t\tthis.authorizator.verifyUploadAccess({ key: localObjectKey, size })\n\n\t\tconst objectKey = (this.config.prefix ? this.config.prefix + '/' : '') + localObjectKey\n\n\t\tconst bucket = this.config.bucket\n\t\tif (acl && this.config.noAcl) {\n\t\t\tthrow new UserInputError('ACL is not supported')\n\t\t}\n\n\t\tconst headers: Record<string, string> = {\n\t\t\t'Cache-Control': 'immutable',\n\t\t}\n\t\tif (contentType) {\n\t\t\theaders['Content-Type'] = contentType\n\t\t}\n\n\t\tif (!this.config.noAcl && acl !== 'NONE') {\n\t\t\tconst mapping = { PUBLIC_READ: 'public-read', PRIVATE: 'private' }\n\t\t\theaders['x-amz-acl'] = mapping[acl ?? 'PUBLIC_READ']\n\t\t}\n\t\tif (fileName || contentDisposition) {\n\t\t\tlet contentDispositionHeader = contentDisposition?.toLowerCase() ?? 'inline'\n\t\t\tif (fileName) {\n\t\t\t\tcontentDispositionHeader += `; filename*=UTF-8''${encodeURIComponent(fileName)}`\n\t\t\t}\n\t\t\theaders['Content-Disposition'] = contentDispositionHeader\n\t\t}\n\t\tconst headersToSend = Object.entries(headers).map(([key, value]) => ({ key, value }))\n\n\t\t// intentionally not sending following headers\n\t\tif (size) {\n\t\t\theaders['Content-Length'] = String(size)\n\t\t}\n\n\t\tconst url = this.signer.sign({\n\t\t\taction: 'upload',\n\t\t\texpiration: expiration ?? 3600,\n\t\t\tkey: objectKey,\n\t\t\theaders: { ...headers },\n\t\t})\n\n\t\tconst publicUrl = this.formatPublicUrl(objectKey)\n\t\treturn {\n\t\t\tbucket,\n\t\t\tobjectKey,\n\t\t\turl,\n\t\t\tpublicUrl,\n\t\t\theaders: headersToSend,\n\t\t\tmethod: 'PUT',\n\t\t}\n\t}\n\n\tpublic getSignedReadUrl({ objectKey, expiration }: { objectKey: string; expiration: number | null }): SignedReadUrl {\n\t\tconst bucket = this.config.bucket\n\n\t\tconst publicPrefix = this.formatPublicUrl('')\n\t\tif (objectKey.startsWith(publicPrefix)) {\n\t\t\tobjectKey = objectKey.substring(publicPrefix.length)\n\t\t}\n\t\tif (this.config.prefix && !objectKey.startsWith(this.config.prefix)) {\n\t\t\tthrow new ForbiddenError(\n\t\t\t\t`Given object key \"${objectKey}\" does not start with a project prefix \"${this.config.prefix}\"`,\n\t\t\t)\n\t\t}\n\t\tconst localObjectKey = this.config.prefix ? objectKey.substring(this.config.prefix.length + 1) : objectKey\n\t\tthis.authorizator.verifyReadAccess({ key: localObjectKey })\n\n\t\tconst url = this.signer.sign({\n\t\t\taction: 'read',\n\t\t\texpiration: expiration ?? 3600,\n\t\t\tkey: objectKey,\n\t\t\theaders: {},\n\t\t})\n\n\t\treturn {\n\t\t\tbucket,\n\t\t\tobjectKey,\n\t\t\turl,\n\t\t\theaders: [],\n\t\t\tmethod: 'GET',\n\t\t}\n\t}\n\n\tpublic formatPublicUrl(key: string): string {\n\t\treturn `${this.publicBaseUrl}/${key}`\n\t}\n}\n\nexport class S3ServiceFactory {\n\tpublic create(config: S3Config, providers: Providers, authorizator: S3ObjectAuthorizator) {\n\t\treturn new S3Service(config, providers, authorizator)\n\t}\n}\n"],"names":["extension","resolveExtension"],"mappings":"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"}
1
+ {"version":3,"file":"S3Service.js","sources":["../../../../packages/engine-s3-plugin/src/S3Service.ts"],"sourcesContent":["import { extension as resolveExtension } from 'mime-types'\nimport { resolveS3PublicBaseUrl, S3Config } from './Config.js'\nimport { ForbiddenError, UserInputError } from '@contember/graphql-utils'\nimport { Providers } from '@contember/engine-plugins'\nimport { S3Signer } from './S3Signer.js'\nimport { S3GenerateSignedUploadInput } from './S3SchemaTypes.js'\nimport { S3ObjectAuthorizator } from './S3ObjectAuthorizator.js'\n\ntype SignedReadUrl = {\n\turl: string\n\tbucket: string\n\tobjectKey: string\n\theaders: { key: string; value: string }[]\n\tmethod: string\n}\n\ntype SignedUploadUrl = {\n\tobjectKey: string\n\turl: string\n\tbucket: string\n\tpublicUrl: string\n\theaders: { key: string; value: string }[]\n\tmethod: string\n}\n\ntype DeletedObject = {\n\tbucket: string\n\tobjectKey: string\n}\n\nexport type S3FetchResponse = {\n\treadonly ok: boolean\n\treadonly status: number\n\ttext(): Promise<string>\n}\n\nexport type S3Fetch = (url: string, init: { method: string; signal: AbortSignal }) => Promise<S3FetchResponse>\n\nconst deleteUrlExpiration = 60\nconst deleteTimeoutMs = 10_000\n\nexport class S3Service {\n\tprivate readonly publicBaseUrl: string\n\n\tprivate readonly signer: S3Signer\n\n\tconstructor(\n\t\tpublic readonly config: S3Config,\n\t\tprivate readonly providers: Pick<Providers, 'uuid' | 'now'>,\n\t\tprivate readonly authorizator: S3ObjectAuthorizator,\n\t\tprivate readonly fetch: S3Fetch = (url, init) => globalThis.fetch(url, init),\n\t) {\n\t\tthis.publicBaseUrl = resolveS3PublicBaseUrl(config)\n\t\tthis.signer = new S3Signer(config, providers)\n\t}\n\n\tpublic getSignedUploadUrl(\n\t\t{ acl, contentDisposition, contentType, expiration, prefix, suffix, fileName, extension, size }: S3GenerateSignedUploadInput,\n\t): SignedUploadUrl {\n\t\tconst ext = extension ?? ((contentType ? resolveExtension(contentType) : null) || 'bin')\n\t\tconst id = this.providers.uuid()\n\t\tconst localObjectKey = (prefix ? prefix + '/' : '') + `${id}${suffix ?? ''}.${ext}`\n\t\tthis.authorizator.verifyUploadAccess({ key: localObjectKey, size })\n\n\t\tconst objectKey = (this.config.prefix ? this.config.prefix + '/' : '') + localObjectKey\n\n\t\tconst bucket = this.config.bucket\n\t\tif (acl && this.config.noAcl) {\n\t\t\tthrow new UserInputError('ACL is not supported')\n\t\t}\n\n\t\tconst headers: Record<string, string> = {\n\t\t\t'Cache-Control': 'immutable',\n\t\t}\n\t\tif (contentType) {\n\t\t\theaders['Content-Type'] = contentType\n\t\t}\n\n\t\tif (!this.config.noAcl && acl !== 'NONE') {\n\t\t\tconst mapping = { PUBLIC_READ: 'public-read', PRIVATE: 'private' }\n\t\t\theaders['x-amz-acl'] = mapping[acl ?? 'PUBLIC_READ']\n\t\t}\n\t\tif (fileName || contentDisposition) {\n\t\t\tlet contentDispositionHeader = contentDisposition?.toLowerCase() ?? 'inline'\n\t\t\tif (fileName) {\n\t\t\t\tcontentDispositionHeader += `; filename*=UTF-8''${encodeURIComponent(fileName)}`\n\t\t\t}\n\t\t\theaders['Content-Disposition'] = contentDispositionHeader\n\t\t}\n\t\tconst headersToSend = Object.entries(headers).map(([key, value]) => ({ key, value }))\n\n\t\t// intentionally not sending following headers\n\t\tif (size) {\n\t\t\theaders['Content-Length'] = String(size)\n\t\t}\n\n\t\tconst url = this.signer.sign({\n\t\t\taction: 'upload',\n\t\t\texpiration: expiration ?? 3600,\n\t\t\tkey: objectKey,\n\t\t\theaders: { ...headers },\n\t\t})\n\n\t\tconst publicUrl = this.formatPublicUrl(objectKey)\n\t\treturn {\n\t\t\tbucket,\n\t\t\tobjectKey,\n\t\t\turl,\n\t\t\tpublicUrl,\n\t\t\theaders: headersToSend,\n\t\t\tmethod: 'PUT',\n\t\t}\n\t}\n\n\tpublic getSignedReadUrl({ objectKey, expiration }: { objectKey: string; expiration: number | null }): SignedReadUrl {\n\t\tconst bucket = this.config.bucket\n\t\tconst resolved = this.resolveObjectKey(objectKey)\n\t\tthis.authorizator.verifyReadAccess({ key: resolved.localObjectKey })\n\n\t\tconst url = this.signer.sign({\n\t\t\taction: 'read',\n\t\t\texpiration: expiration ?? 3600,\n\t\t\tkey: resolved.objectKey,\n\t\t\theaders: {},\n\t\t})\n\n\t\treturn {\n\t\t\tbucket,\n\t\t\tobjectKey: resolved.objectKey,\n\t\t\turl,\n\t\t\theaders: [],\n\t\t\tmethod: 'GET',\n\t\t}\n\t}\n\n\t/**\n\t * Unlike an upload or a read, a delete carries no payload, so the signed URL never leaves the server.\n\t */\n\tpublic async deleteObject({ objectKey }: { objectKey: string }): Promise<DeletedObject> {\n\t\tconst bucket = this.config.bucket\n\t\tconst resolved = this.resolveObjectKey(objectKey)\n\t\tthis.authorizator.verifyDeleteAccess({ key: resolved.localObjectKey })\n\n\t\tconst url = this.signer.sign({\n\t\t\taction: 'delete',\n\t\t\texpiration: deleteUrlExpiration,\n\t\t\tkey: resolved.objectKey,\n\t\t\theaders: {},\n\t\t})\n\n\t\tconst response = await this.fetch(url, { method: 'DELETE', signal: AbortSignal.timeout(deleteTimeoutMs) })\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to delete an object \"${resolved.objectKey}\": S3 responded with ${response.status}: ${await response.text()}`)\n\t\t}\n\n\t\treturn {\n\t\t\tbucket,\n\t\t\tobjectKey: resolved.objectKey,\n\t\t}\n\t}\n\n\tpublic formatPublicUrl(key: string): string {\n\t\treturn `${this.publicBaseUrl}/${key}`\n\t}\n\n\tprivate resolveObjectKey(objectKey: string): { objectKey: string; localObjectKey: string } {\n\t\tconst publicPrefix = this.formatPublicUrl('')\n\t\tif (objectKey.startsWith(publicPrefix)) {\n\t\t\tobjectKey = objectKey.substring(publicPrefix.length)\n\t\t}\n\t\t// the separator is a part of the prefix: without it, a project prefixed \"blog\" reaches into \"blog-staging\" in a shared bucket\n\t\tconst prefix = this.config.prefix ? this.config.prefix + '/' : ''\n\t\tif (prefix && !objectKey.startsWith(prefix)) {\n\t\t\tthrow new ForbiddenError(\n\t\t\t\t`Given object key \"${objectKey}\" does not start with a project prefix \"${this.config.prefix}\"`,\n\t\t\t)\n\t\t}\n\t\tconst localObjectKey = objectKey.substring(prefix.length)\n\n\t\treturn { objectKey, localObjectKey }\n\t}\n}\n\nexport class S3ServiceFactory {\n\tpublic create(config: S3Config, providers: Providers, authorizator: S3ObjectAuthorizator) {\n\t\treturn new S3Service(config, providers, authorizator)\n\t}\n}\n"],"names":["extension","resolveExtension"],"mappings":"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"}
@@ -6,6 +6,11 @@ const crypto = require("node:crypto");
6
6
  var __defProp = Object.defineProperty;
7
7
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
8
  var __publicField = (obj, key, value) => __defNormalProp(obj, key + "", value);
9
+ const methods = {
10
+ read: "GET",
11
+ upload: "PUT",
12
+ delete: "DELETE"
13
+ };
9
14
  const hmac = (secret, data) => crypto.createHmac("sha256", secret).update(data).digest();
10
15
  const hash = (data) => crypto.createHash("sha256").update(data).digest("hex");
11
16
  const unsignableHeaders = ["authorization", "user-agent", "content-type", "expect", "x-amzn-trace-id"];
@@ -26,7 +31,7 @@ class S3Signer {
26
31
  const queryString = this.getQueryParams(request, nowFormatted);
27
32
  const path = uris.basePath + "/" + request.key;
28
33
  const lines = [
29
- request.action === "read" ? "GET" : "PUT",
34
+ methods[request.action],
30
35
  path,
31
36
  queryString,
32
37
  this.getSignedHeaders(request),
@@ -1 +1 @@
1
- {"version":3,"file":"S3Signer.cjs","sources":["../../../../packages/engine-s3-plugin/src/S3Signer.ts"],"sourcesContent":["import { resolveS3Endpoint, S3Config } from './Config.js'\nimport { Providers } from '@contember/engine-plugins'\nimport qs from 'qs'\nimport { BinaryLike, createHash, createHmac } from 'node:crypto'\n\ninterface S3Request {\n\taction: 'read' | 'upload'\n\tkey: string\n\texpiration: number\n\theaders: Record<string, string>\n}\n\nconst hmac = (secret: BinaryLike, data: BinaryLike) => createHmac('sha256', secret).update(data).digest()\n\nconst hash = (data: BinaryLike) => createHash('sha256').update(data).digest('hex')\n\nconst unsignableHeaders = ['authorization', 'user-agent', 'content-type', 'expect', 'x-amzn-trace-id']\n\nconst sortMap = <T extends Record<string, any>>(input: T): T => {\n\treturn Object.fromEntries(Object.entries(input).sort((a, b) => (a[0] < b[0] ? -1 : 1))) as T\n}\nexport class S3Signer {\n\tprivate region: string\n\n\tconstructor(private readonly config: S3Config, private readonly providers: Pick<Providers, 'now'>) {\n\t\tthis.region = config.region || 'eu-west-1'\n\t}\n\n\tpublic sign(request: S3Request) {\n\t\tconst uris = resolveS3Endpoint(this.config)\n\t\trequest.headers.host = uris.endpoint.substring(uris.endpoint.indexOf('://') + 3)\n\t\tconst nowFormatted = this.providers\n\t\t\t.now()\n\t\t\t.toISOString()\n\t\t\t.replace(/[:-]|\\.\\d{3}/g, '')\n\t\tconst queryString = this.getQueryParams(request, nowFormatted)\n\t\tconst path = uris.basePath + '/' + request.key\n\t\tconst lines = [\n\t\t\trequest.action === 'read' ? 'GET' : 'PUT',\n\t\t\tpath,\n\t\t\tqueryString,\n\t\t\tthis.getSignedHeaders(request),\n\t\t\tthis.getSignedHeaderNames(Object.keys(request.headers)),\n\t\t\t'UNSIGNED-PAYLOAD',\n\t\t]\n\t\tconst canonicalRequest = lines.join('\\n')\n\n\t\tconst parts = ['AWS4-HMAC-SHA256', nowFormatted, this.getScope(nowFormatted), hash(canonicalRequest)]\n\n\t\tconst signature = [nowFormatted.substring(0, 8), this.region, 's3', 'aws4_request', parts.join('\\n')]\n\t\t\t.reduce<Buffer>((acc, x) => hmac(acc, x), Buffer.from('AWS4' + this.config.credentials.secret))\n\t\t\t.toString('hex')\n\t\treturn uris.endpoint + path + '?' + (queryString + '&X-Amz-Signature=' + signature).split('&').sort().join('&')\n\t}\n\n\tprivate getQueryParams(request: S3Request, dateFormatted: string) {\n\t\tconst scope = this.getScope(dateFormatted)\n\t\tconst queryString: Record<string, string> = {\n\t\t\t'X-Amz-Date': dateFormatted,\n\t\t\t'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',\n\t\t\t'X-Amz-Credential': this.config.credentials.key + '/' + scope,\n\t\t\t'X-Amz-Expires': String(request.expiration),\n\t\t\t'X-Amz-SignedHeaders': this.getSignedHeaderNames(Object.keys(request.headers)),\n\t\t}\n\n\t\tconst headersToQs = ['content-type', 'content-md5', 'cache-control']\n\t\tfor (const header in request.headers) {\n\t\t\tconst headerLc = header.toLowerCase()\n\t\t\tif (headersToQs.includes(headerLc) || headerLc.startsWith('x-amz-')) {\n\t\t\t\tqueryString[headerLc.startsWith('x-amz-meta-') ? headerLc : header] = request.headers[header]\n\t\t\t}\n\t\t}\n\t\tconst qsSorted = sortMap(queryString)\n\t\treturn qs.stringify(qsSorted)\n\t}\n\n\tprivate getScope(dateFormatted: string) {\n\t\treturn dateFormatted.substring(0, 8) + '/' + this.region + '/s3/aws4_request'\n\t}\n\n\tprivate getSignedHeaders(request: S3Request): string {\n\t\tconst lines = []\n\t\tfor (const header in request.headers) {\n\t\t\tconst headerNameLc = header.toLowerCase()\n\t\t\tif (!this.isHeaderSignable(headerNameLc)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst value = request.headers[header].replace(/\\s+/g, ' ').replace(/^\\s+|\\s+$/g, '')\n\t\t\tlines.push(`${headerNameLc}:${value}\\n`)\n\t\t}\n\t\treturn lines.sort().join('')\n\t}\n\n\tprivate isHeaderSignable(headerName: string) {\n\t\treturn headerName.toLowerCase().startsWith('x-amz') || !unsignableHeaders.includes(headerName.toLowerCase())\n\t}\n\n\tprivate getSignedHeaderNames(headers: string[]) {\n\t\treturn headers\n\t\t\t.map(it => it.toLowerCase())\n\t\t\t.filter(this.isHeaderSignable)\n\t\t\t.sort()\n\t\t\t.join(';')\n\t}\n}\n"],"names":["createHmac","createHash","resolveS3Endpoint"],"mappings":"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"}
1
+ {"version":3,"file":"S3Signer.cjs","sources":["../../../../packages/engine-s3-plugin/src/S3Signer.ts"],"sourcesContent":["import { resolveS3Endpoint, S3Config } from './Config.js'\nimport { Providers } from '@contember/engine-plugins'\nimport qs from 'qs'\nimport { BinaryLike, createHash, createHmac } from 'node:crypto'\n\ninterface S3Request {\n\taction: 'read' | 'upload' | 'delete'\n\tkey: string\n\texpiration: number\n\theaders: Record<string, string>\n}\n\nconst methods: Record<S3Request['action'], string> = {\n\tread: 'GET',\n\tupload: 'PUT',\n\tdelete: 'DELETE',\n}\n\nconst hmac = (secret: BinaryLike, data: BinaryLike) => createHmac('sha256', secret).update(data).digest()\n\nconst hash = (data: BinaryLike) => createHash('sha256').update(data).digest('hex')\n\nconst unsignableHeaders = ['authorization', 'user-agent', 'content-type', 'expect', 'x-amzn-trace-id']\n\nconst sortMap = <T extends Record<string, any>>(input: T): T => {\n\treturn Object.fromEntries(Object.entries(input).sort((a, b) => (a[0] < b[0] ? -1 : 1))) as T\n}\nexport class S3Signer {\n\tprivate region: string\n\n\tconstructor(private readonly config: S3Config, private readonly providers: Pick<Providers, 'now'>) {\n\t\tthis.region = config.region || 'eu-west-1'\n\t}\n\n\tpublic sign(request: S3Request) {\n\t\tconst uris = resolveS3Endpoint(this.config)\n\t\trequest.headers.host = uris.endpoint.substring(uris.endpoint.indexOf('://') + 3)\n\t\tconst nowFormatted = this.providers\n\t\t\t.now()\n\t\t\t.toISOString()\n\t\t\t.replace(/[:-]|\\.\\d{3}/g, '')\n\t\tconst queryString = this.getQueryParams(request, nowFormatted)\n\t\tconst path = uris.basePath + '/' + request.key\n\t\tconst lines = [\n\t\t\tmethods[request.action],\n\t\t\tpath,\n\t\t\tqueryString,\n\t\t\tthis.getSignedHeaders(request),\n\t\t\tthis.getSignedHeaderNames(Object.keys(request.headers)),\n\t\t\t'UNSIGNED-PAYLOAD',\n\t\t]\n\t\tconst canonicalRequest = lines.join('\\n')\n\n\t\tconst parts = ['AWS4-HMAC-SHA256', nowFormatted, this.getScope(nowFormatted), hash(canonicalRequest)]\n\n\t\tconst signature = [nowFormatted.substring(0, 8), this.region, 's3', 'aws4_request', parts.join('\\n')]\n\t\t\t.reduce<Buffer>((acc, x) => hmac(acc, x), Buffer.from('AWS4' + this.config.credentials.secret))\n\t\t\t.toString('hex')\n\t\treturn uris.endpoint + path + '?' + (queryString + '&X-Amz-Signature=' + signature).split('&').sort().join('&')\n\t}\n\n\tprivate getQueryParams(request: S3Request, dateFormatted: string) {\n\t\tconst scope = this.getScope(dateFormatted)\n\t\tconst queryString: Record<string, string> = {\n\t\t\t'X-Amz-Date': dateFormatted,\n\t\t\t'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',\n\t\t\t'X-Amz-Credential': this.config.credentials.key + '/' + scope,\n\t\t\t'X-Amz-Expires': String(request.expiration),\n\t\t\t'X-Amz-SignedHeaders': this.getSignedHeaderNames(Object.keys(request.headers)),\n\t\t}\n\n\t\tconst headersToQs = ['content-type', 'content-md5', 'cache-control']\n\t\tfor (const header in request.headers) {\n\t\t\tconst headerLc = header.toLowerCase()\n\t\t\tif (headersToQs.includes(headerLc) || headerLc.startsWith('x-amz-')) {\n\t\t\t\tqueryString[headerLc.startsWith('x-amz-meta-') ? headerLc : header] = request.headers[header]\n\t\t\t}\n\t\t}\n\t\tconst qsSorted = sortMap(queryString)\n\t\treturn qs.stringify(qsSorted)\n\t}\n\n\tprivate getScope(dateFormatted: string) {\n\t\treturn dateFormatted.substring(0, 8) + '/' + this.region + '/s3/aws4_request'\n\t}\n\n\tprivate getSignedHeaders(request: S3Request): string {\n\t\tconst lines = []\n\t\tfor (const header in request.headers) {\n\t\t\tconst headerNameLc = header.toLowerCase()\n\t\t\tif (!this.isHeaderSignable(headerNameLc)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst value = request.headers[header].replace(/\\s+/g, ' ').replace(/^\\s+|\\s+$/g, '')\n\t\t\tlines.push(`${headerNameLc}:${value}\\n`)\n\t\t}\n\t\treturn lines.sort().join('')\n\t}\n\n\tprivate isHeaderSignable(headerName: string) {\n\t\treturn headerName.toLowerCase().startsWith('x-amz') || !unsignableHeaders.includes(headerName.toLowerCase())\n\t}\n\n\tprivate getSignedHeaderNames(headers: string[]) {\n\t\treturn headers\n\t\t\t.map(it => it.toLowerCase())\n\t\t\t.filter(this.isHeaderSignable)\n\t\t\t.sort()\n\t\t\t.join(';')\n\t}\n}\n"],"names":["createHmac","createHash","resolveS3Endpoint"],"mappings":"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"}
@@ -4,6 +4,11 @@ import { createHash, createHmac } from "node:crypto";
4
4
  var __defProp = Object.defineProperty;
5
5
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
6
  var __publicField = (obj, key, value) => __defNormalProp(obj, key + "", value);
7
+ const methods = {
8
+ read: "GET",
9
+ upload: "PUT",
10
+ delete: "DELETE"
11
+ };
7
12
  const hmac = (secret, data) => createHmac("sha256", secret).update(data).digest();
8
13
  const hash = (data) => createHash("sha256").update(data).digest("hex");
9
14
  const unsignableHeaders = ["authorization", "user-agent", "content-type", "expect", "x-amzn-trace-id"];
@@ -24,7 +29,7 @@ class S3Signer {
24
29
  const queryString = this.getQueryParams(request, nowFormatted);
25
30
  const path = uris.basePath + "/" + request.key;
26
31
  const lines = [
27
- request.action === "read" ? "GET" : "PUT",
32
+ methods[request.action],
28
33
  path,
29
34
  queryString,
30
35
  this.getSignedHeaders(request),
@@ -1 +1 @@
1
- {"version":3,"file":"S3Signer.js","sources":["../../../../packages/engine-s3-plugin/src/S3Signer.ts"],"sourcesContent":["import { resolveS3Endpoint, S3Config } from './Config.js'\nimport { Providers } from '@contember/engine-plugins'\nimport qs from 'qs'\nimport { BinaryLike, createHash, createHmac } from 'node:crypto'\n\ninterface S3Request {\n\taction: 'read' | 'upload'\n\tkey: string\n\texpiration: number\n\theaders: Record<string, string>\n}\n\nconst hmac = (secret: BinaryLike, data: BinaryLike) => createHmac('sha256', secret).update(data).digest()\n\nconst hash = (data: BinaryLike) => createHash('sha256').update(data).digest('hex')\n\nconst unsignableHeaders = ['authorization', 'user-agent', 'content-type', 'expect', 'x-amzn-trace-id']\n\nconst sortMap = <T extends Record<string, any>>(input: T): T => {\n\treturn Object.fromEntries(Object.entries(input).sort((a, b) => (a[0] < b[0] ? -1 : 1))) as T\n}\nexport class S3Signer {\n\tprivate region: string\n\n\tconstructor(private readonly config: S3Config, private readonly providers: Pick<Providers, 'now'>) {\n\t\tthis.region = config.region || 'eu-west-1'\n\t}\n\n\tpublic sign(request: S3Request) {\n\t\tconst uris = resolveS3Endpoint(this.config)\n\t\trequest.headers.host = uris.endpoint.substring(uris.endpoint.indexOf('://') + 3)\n\t\tconst nowFormatted = this.providers\n\t\t\t.now()\n\t\t\t.toISOString()\n\t\t\t.replace(/[:-]|\\.\\d{3}/g, '')\n\t\tconst queryString = this.getQueryParams(request, nowFormatted)\n\t\tconst path = uris.basePath + '/' + request.key\n\t\tconst lines = [\n\t\t\trequest.action === 'read' ? 'GET' : 'PUT',\n\t\t\tpath,\n\t\t\tqueryString,\n\t\t\tthis.getSignedHeaders(request),\n\t\t\tthis.getSignedHeaderNames(Object.keys(request.headers)),\n\t\t\t'UNSIGNED-PAYLOAD',\n\t\t]\n\t\tconst canonicalRequest = lines.join('\\n')\n\n\t\tconst parts = ['AWS4-HMAC-SHA256', nowFormatted, this.getScope(nowFormatted), hash(canonicalRequest)]\n\n\t\tconst signature = [nowFormatted.substring(0, 8), this.region, 's3', 'aws4_request', parts.join('\\n')]\n\t\t\t.reduce<Buffer>((acc, x) => hmac(acc, x), Buffer.from('AWS4' + this.config.credentials.secret))\n\t\t\t.toString('hex')\n\t\treturn uris.endpoint + path + '?' + (queryString + '&X-Amz-Signature=' + signature).split('&').sort().join('&')\n\t}\n\n\tprivate getQueryParams(request: S3Request, dateFormatted: string) {\n\t\tconst scope = this.getScope(dateFormatted)\n\t\tconst queryString: Record<string, string> = {\n\t\t\t'X-Amz-Date': dateFormatted,\n\t\t\t'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',\n\t\t\t'X-Amz-Credential': this.config.credentials.key + '/' + scope,\n\t\t\t'X-Amz-Expires': String(request.expiration),\n\t\t\t'X-Amz-SignedHeaders': this.getSignedHeaderNames(Object.keys(request.headers)),\n\t\t}\n\n\t\tconst headersToQs = ['content-type', 'content-md5', 'cache-control']\n\t\tfor (const header in request.headers) {\n\t\t\tconst headerLc = header.toLowerCase()\n\t\t\tif (headersToQs.includes(headerLc) || headerLc.startsWith('x-amz-')) {\n\t\t\t\tqueryString[headerLc.startsWith('x-amz-meta-') ? headerLc : header] = request.headers[header]\n\t\t\t}\n\t\t}\n\t\tconst qsSorted = sortMap(queryString)\n\t\treturn qs.stringify(qsSorted)\n\t}\n\n\tprivate getScope(dateFormatted: string) {\n\t\treturn dateFormatted.substring(0, 8) + '/' + this.region + '/s3/aws4_request'\n\t}\n\n\tprivate getSignedHeaders(request: S3Request): string {\n\t\tconst lines = []\n\t\tfor (const header in request.headers) {\n\t\t\tconst headerNameLc = header.toLowerCase()\n\t\t\tif (!this.isHeaderSignable(headerNameLc)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst value = request.headers[header].replace(/\\s+/g, ' ').replace(/^\\s+|\\s+$/g, '')\n\t\t\tlines.push(`${headerNameLc}:${value}\\n`)\n\t\t}\n\t\treturn lines.sort().join('')\n\t}\n\n\tprivate isHeaderSignable(headerName: string) {\n\t\treturn headerName.toLowerCase().startsWith('x-amz') || !unsignableHeaders.includes(headerName.toLowerCase())\n\t}\n\n\tprivate getSignedHeaderNames(headers: string[]) {\n\t\treturn headers\n\t\t\t.map(it => it.toLowerCase())\n\t\t\t.filter(this.isHeaderSignable)\n\t\t\t.sort()\n\t\t\t.join(';')\n\t}\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"S3Signer.js","sources":["../../../../packages/engine-s3-plugin/src/S3Signer.ts"],"sourcesContent":["import { resolveS3Endpoint, S3Config } from './Config.js'\nimport { Providers } from '@contember/engine-plugins'\nimport qs from 'qs'\nimport { BinaryLike, createHash, createHmac } from 'node:crypto'\n\ninterface S3Request {\n\taction: 'read' | 'upload' | 'delete'\n\tkey: string\n\texpiration: number\n\theaders: Record<string, string>\n}\n\nconst methods: Record<S3Request['action'], string> = {\n\tread: 'GET',\n\tupload: 'PUT',\n\tdelete: 'DELETE',\n}\n\nconst hmac = (secret: BinaryLike, data: BinaryLike) => createHmac('sha256', secret).update(data).digest()\n\nconst hash = (data: BinaryLike) => createHash('sha256').update(data).digest('hex')\n\nconst unsignableHeaders = ['authorization', 'user-agent', 'content-type', 'expect', 'x-amzn-trace-id']\n\nconst sortMap = <T extends Record<string, any>>(input: T): T => {\n\treturn Object.fromEntries(Object.entries(input).sort((a, b) => (a[0] < b[0] ? -1 : 1))) as T\n}\nexport class S3Signer {\n\tprivate region: string\n\n\tconstructor(private readonly config: S3Config, private readonly providers: Pick<Providers, 'now'>) {\n\t\tthis.region = config.region || 'eu-west-1'\n\t}\n\n\tpublic sign(request: S3Request) {\n\t\tconst uris = resolveS3Endpoint(this.config)\n\t\trequest.headers.host = uris.endpoint.substring(uris.endpoint.indexOf('://') + 3)\n\t\tconst nowFormatted = this.providers\n\t\t\t.now()\n\t\t\t.toISOString()\n\t\t\t.replace(/[:-]|\\.\\d{3}/g, '')\n\t\tconst queryString = this.getQueryParams(request, nowFormatted)\n\t\tconst path = uris.basePath + '/' + request.key\n\t\tconst lines = [\n\t\t\tmethods[request.action],\n\t\t\tpath,\n\t\t\tqueryString,\n\t\t\tthis.getSignedHeaders(request),\n\t\t\tthis.getSignedHeaderNames(Object.keys(request.headers)),\n\t\t\t'UNSIGNED-PAYLOAD',\n\t\t]\n\t\tconst canonicalRequest = lines.join('\\n')\n\n\t\tconst parts = ['AWS4-HMAC-SHA256', nowFormatted, this.getScope(nowFormatted), hash(canonicalRequest)]\n\n\t\tconst signature = [nowFormatted.substring(0, 8), this.region, 's3', 'aws4_request', parts.join('\\n')]\n\t\t\t.reduce<Buffer>((acc, x) => hmac(acc, x), Buffer.from('AWS4' + this.config.credentials.secret))\n\t\t\t.toString('hex')\n\t\treturn uris.endpoint + path + '?' + (queryString + '&X-Amz-Signature=' + signature).split('&').sort().join('&')\n\t}\n\n\tprivate getQueryParams(request: S3Request, dateFormatted: string) {\n\t\tconst scope = this.getScope(dateFormatted)\n\t\tconst queryString: Record<string, string> = {\n\t\t\t'X-Amz-Date': dateFormatted,\n\t\t\t'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',\n\t\t\t'X-Amz-Credential': this.config.credentials.key + '/' + scope,\n\t\t\t'X-Amz-Expires': String(request.expiration),\n\t\t\t'X-Amz-SignedHeaders': this.getSignedHeaderNames(Object.keys(request.headers)),\n\t\t}\n\n\t\tconst headersToQs = ['content-type', 'content-md5', 'cache-control']\n\t\tfor (const header in request.headers) {\n\t\t\tconst headerLc = header.toLowerCase()\n\t\t\tif (headersToQs.includes(headerLc) || headerLc.startsWith('x-amz-')) {\n\t\t\t\tqueryString[headerLc.startsWith('x-amz-meta-') ? headerLc : header] = request.headers[header]\n\t\t\t}\n\t\t}\n\t\tconst qsSorted = sortMap(queryString)\n\t\treturn qs.stringify(qsSorted)\n\t}\n\n\tprivate getScope(dateFormatted: string) {\n\t\treturn dateFormatted.substring(0, 8) + '/' + this.region + '/s3/aws4_request'\n\t}\n\n\tprivate getSignedHeaders(request: S3Request): string {\n\t\tconst lines = []\n\t\tfor (const header in request.headers) {\n\t\t\tconst headerNameLc = header.toLowerCase()\n\t\t\tif (!this.isHeaderSignable(headerNameLc)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst value = request.headers[header].replace(/\\s+/g, ' ').replace(/^\\s+|\\s+$/g, '')\n\t\t\tlines.push(`${headerNameLc}:${value}\\n`)\n\t\t}\n\t\treturn lines.sort().join('')\n\t}\n\n\tprivate isHeaderSignable(headerName: string) {\n\t\treturn headerName.toLowerCase().startsWith('x-amz') || !unsignableHeaders.includes(headerName.toLowerCase())\n\t}\n\n\tprivate getSignedHeaderNames(headers: string[]) {\n\t\treturn headers\n\t\t\t.map(it => it.toLowerCase())\n\t\t\t.filter(this.isHeaderSignable)\n\t\t\t.sort()\n\t\t\t.join(';')\n\t}\n}\n"],"names":[],"mappings":"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"}