├── .gitignore ├── LICENSE ├── README.md └── lambda ├── configure.js ├── helper.js ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019, Native Documents Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | The Software uses certain additional modules, including @nativedocuments/docx-wasm, which are subject to their own licenses, licensed separately, and outside the scope of the above copyright notice. Copies of the licenses for these modules are available upon request. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docx-to-pdf 2 | 3 | ## Description 4 | 5 | Convert a Word document (.doc or .docx) in a source S3 bucket to PDF, saving the PDF to a destination S3 bucket. 6 | 7 | This Lambda can be invoked from an AWS Step Function, or in response to an S3 "created" or SQS event. It could 8 | easily be modified to support other triggers. You probably still want to use S3 buckets, to workaround 9 | any limits on request/response size. 10 | 11 | Thanks to Lambda's concurrency, this approach is well-suited to variable bulk/batch higher-volume conversion workloads. 12 | 13 | This app uses Native Documents' [docx-wasm](https://www.npmjs.com/package/@nativedocuments/docx-wasm) to perform the conversion. It does not use LibreOffice etc. 14 | 15 | ## Installation and Getting Started 16 | 17 | Direct link to deploy https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:992364115735:applications~docx-to-pdf or search for "docx-to-pdf" in the Serverless Application Repository. 18 | 19 | Please double check you are in the AWS region you intend; this needs to be the same region as the bucket which will contain the Word documents you wish to convert. 20 | 21 | After you click "Deploy" (bottom right corner), you'll need to wait a minute or so as CloudFormation creates resources. When this is complete, you should see a green tick saying "Your application has been deployed" 22 | 23 | Now go into the function: Lambda > Functions then configure an S3 or SQS trigger (or your step function), environment variables and execution role as explained below. 24 | 25 | ### S3 Trigger 26 | 27 | This function can respond to S3 ObjectCreated events. In this case, the output PDF is the input key + .pdf. 28 | 29 | To configure the trigger, in "Designer > Add triggers", click "S3". The "Configure triggers" dialog appears. 30 | 31 | * Select a bucket (any time a docx is added to this bucket, the function will run) 32 | 33 | * Verify that "all object create events" is selected (or choose PUT POST or COPY) 34 | 35 | Click "Add" (bottom right), then "Save" (top right). 36 | 37 | ### SQS Trigger 38 | 39 | This function can respond to an SQS event, the event being a message is available on a queue you have configured in the Lambda console ("Designer > Add triggers", click "SQS"). 40 | 41 | The message should contain a body like the following: 42 | 43 | ``` 44 | { 45 | "Records": [ 46 | { 47 | "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", 48 | "receiptHandle": "MessageReceiptHandle", 49 | "body": { 50 | "source_bucket": "YOUR_INPUT_BUCKET_NAME", 51 | "source_key": "YOUR.docx", 52 | "target_bucket": "YOUR_OUTPUT_BUCKET_NAME", 53 | "target_key": "YOUR.pdf" 54 | }, 55 | "attributes": { 56 | "ApproximateReceiveCount": "1", 57 | "SentTimestamp": "1523232000000", 58 | "SenderId": "123456789012", 59 | "ApproximateFirstReceiveTimestamp": "1523232000001" 60 | }, 61 | "messageAttributes": { "correlationId": "foo123"}, 62 | "md5OfBody": "7b270e59b47ff90a553787216d55d91d", 63 | "eventSource": "aws:sqs", 64 | "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:MyQueue", 65 | "awsRegion": "us-west-2" 66 | } 67 | ] 68 | } 69 | ``` 70 | 71 | The body tells the function where to find the input docx, and where to write the output PDF. 72 | 73 | correlationId is as optional message attribute you can use if you wish. 74 | 75 | The function will also write out an SQS message, if you have set an environment variable named SQS_WRITE_QUEUE_URL, specifying a queue. 76 | 77 | This message contains body: 78 | 79 | ``` 80 | {"bucket":"YOUR_OUTPUT_BUCKET_NAME","key":"YOUR.pdf"} 81 | ``` 82 | 83 | and if the function was triggered by an SQS message with a correlationId, that 84 | correlationId may be found in the message attributes. 85 | 86 | ### AWS Step Function 87 | 88 | If you want to use docx-to-pdf in an AWS Step Function, you don't need either of the above triggers. 89 | 90 | Instead, add a state of type task. Here is a working demo step function: 91 | 92 | ``` 93 | { 94 | "Comment": "docx-to-pdf conversion step", 95 | "StartAt": "DocxToPdf", 96 | "States": { 97 | "DocxToPdf": { 98 | "Type": "Task", 99 | "Resource": "arn:aws:lambda:us-west-2:992364999735:function:cloud9-serverlessrepo-docx-to-pdf-S3Fn-Z2UN6XIPAZ8B", 100 | "Parameters": { 101 | "source_bucket.$": "$.source_bucket", 102 | "source_key.$": "$.source_key", 103 | "target_bucket.$": "$.target_bucket", 104 | "target_key.$": "$.target_key" 105 | }, 106 | "End": true 107 | } 108 | } 109 | } 110 | 111 | ``` 112 | 113 | Replace the Resource value with the ARN for your Lambda (shown in the Lambda Designer, top right). 114 | 115 | Ensure that step function's IAM role has permission to execute the Lambda. 116 | 117 | 118 | ### Registration 119 | 120 | This application uses Native Documents docx-wasm library to perform the conversion. 121 | 122 | So you need a ND\_DEV\_ID, ND\_DEV\_SECRET pair (or ND\_LICENSE\_URL) to use it. We have a generous free tier, you can get your keys at https://developers.nativedocuments.com/ 123 | 124 | Now set these as environment vars in the Lambda console, as described below. 125 | 126 | ### Environment Variables 127 | 128 | On the same screen in the Lambda Management Console for this function, scroll down to "Environment Variables": 129 | 130 | * **ND_DEV_ID**: get this value from https://developers.nativedocuments.com/ (see Registration above) 131 | 132 | * **ND_DEV_SECRET**: as above 133 | 134 | * **DEPLOY_ENV**: if 'PROD', don't write debug level logging 135 | 136 | If you are using an S3 trigger, you also need: 137 | 138 | * **S3_BUCKET_OUTPUT**: the name of the S3 bucket to which the PDF will be saved (if blank, it should write to the input event bucket) 139 | 140 | If not, you can remove S3_BUCKET_OUTPUT 141 | 142 | If you want to write an SQS message when a conversion is done, you also need: 143 | 144 | * **SQS_WRITE_QUEUE_URL**: the URL of the queue to which the message should be sent 145 | 146 | 147 | ### Execution role 148 | 149 | Choose or create an execution role. In IAM, confirm that role's policies includes CloudWatch Logs permissions, and also: 150 | 151 | ``` 152 | { 153 | "Effect": "Allow", 154 | "Action": [ 155 | "s3:GetObject", 156 | "s3:PutObject" 157 | ], 158 | "Resource": "arn:aws:s3:::*" 159 | } 160 | ``` 161 | 162 | Without GetObject permission on the triggering bucket and PutObject permission on the output bucket, you'll get Access Denied errors. 163 | 164 | If you are using SQS, also ensure that the Lambda's IAM role has permission to access SQS. 165 | 166 | 167 | ### Confirm installation is successful 168 | 169 | ### S3 Trigger 170 | 171 | If you configured the S3 trigger, you can try it, by copying a Word document (doc or docx) into the S3 bucket you have set the trigger on. 172 | 173 | To verify it works, look for a PDF in your output bucket, or check the logs in cloudwatch 174 | 175 | ### SQS Trigger 176 | 177 | If you configured the SQS trigger, you can try it from the Lambda console 178 | by configuring a test event based on the Records json above. 179 | 180 | To verify it works, look for a PDF in the output bucket you specified, or check the logs in cloudwatch. 181 | 182 | If you specified SQS_WRITE_QUEUE_URL, you can check for an output message 183 | in the SQS Management Console (Queue Actions > View/Delete Messages). 184 | 185 | ### AWS Step Function 186 | 187 | If you used the sample step function, you can "start execution", then for the input, use something like: 188 | 189 | ``` 190 | { 191 | "source_bucket": "MyDocxBucket", 192 | "source_key": "path/to/my/docx", 193 | "target_bucket": "MyPDFBucket", 194 | "target_key": "path/to/my/PDF" 195 | } 196 | ``` 197 | 198 | substituting your own values. 199 | 200 | To verify it works, check the execution status and/or event history, or look for a PDF in your target bucket at target key. You can also check the logs in cloudwatch 201 | 202 | ## Logging 203 | 204 | By default, this application logs to CloudWatch at "debug" level. You can turn debug level logging off by setting environment variable DEPLOY_ENV=PROD. 205 | 206 | ## Sizing notes 207 | 208 | Conversion is processor bound. Lambda allocates processor in proportion to memory. 209 | 210 | Experience suggests 2048MB results in enough processor that conversion is both faster and cheaper (on Lambda January 2019 pricing) than lesser amounts. Note: Conversion uses a minimum of around 500MB RAM, and WebAssembly under Node can use a maximum of 2GB RAM. 211 | 212 | 213 | ## Source code 214 | 215 | To use the source code, clone the GitHub repo. 216 | 217 | In the lambda dir, run `npm install` 218 | 219 | Now you can use your favourite lambda development environment... 220 | 221 | 222 | ## Troubleshooting 223 | 224 | If you are having trouble, please check the CloudWatch logs. 225 | 226 | For each conversion job, "RESULT: Success" or "RESULT: Failed" will be logged. 227 | 228 | Checking for conversion failures: 229 | 230 | * if you are using an S3 trigger, then in the case of "RESULT: Failed", the source document will be copied to the desitnation bucket under the key "BROKEN". This makes it easy to check for conversion failures. If there is a failure, the reason for that failure will appear in the CloudWatch logs. 231 | 232 | * if you are using the demo step function, the in the Step Functions Management Console, you can look for executions with status "failed". If you click into one of these, you'll see the reason in the execution event history, under "ExecutionFailed". 233 | 234 | Here is what may be going wrong: 235 | 236 | * Lambda "Task timed out" or out of memory ("Process exited before completing request"). You can increase Memory and Timeout parameters in the Lambda Management console. 237 | 238 | * **Network error** A network connection is required to validate your ND\_DEV\_ID, ND\_DEV\_SECRET (but not to perform the actual conversion) 239 | 240 | * **TokenValidationError** mean an invalid ND\_DEV\_ID, ND\_DEV\_SECRET pair. Did you get these from https://developers.nativedocuments.com/ and declare them in the Lambda console? 241 | 242 | * **OperationFailedError** Mainly thrown when loading a document. Is this a Word (doc/docx) document? Please verify it opens correctly in Microsoft Word, or drag/drop it to https://canary.nativedocuments.com/ If you continue to have issues, please try a simple "Hello World" document. 243 | 244 | * **EngineExceptionError** An exception in the document engine occured. Please let us know about this! 245 | 246 | 247 | ## Getting Help 248 | 249 | If you continue to have problems, please ask a question on StackOverflow, using tags #docx-wasm, #ms-word, #pdf, #aws-lambda, and #amazon-s3 or #aws-step-functions as appropriate, or [post an issue on GitHub](https://github.com/NativeDocuments/docx-to-pdf-on-AWS-Lambda/issues). 250 | 251 | 252 | -------------------------------------------------------------------------------- /lambda/configure.js: -------------------------------------------------------------------------------- 1 | var docx = require("@nativedocuments/docx-wasm"); 2 | const log = require('lambda-log'); 3 | 4 | exports.init = function (memoryLimitInMB) { 5 | 6 | var ndHeap, ndStream, ndScratch; 7 | if (memoryLimitInMB>750) { 8 | // 2048 MB memory is recommended: 9 | // - on lambda, CPU is proportional to Memory, and we are processor bound 10 | // - on a test workload, 2 GB is faster and cheaper the smaller configs 11 | // - WebAssembly can't use more than 2 GB 12 | var mAvail = memoryLimitInMB - 64; // node overhead, tweak this guestimate? 13 | if (mAvail>2040) { 14 | // Avoid RangeError: WebAssembly.Memory(): Property value 32848 is above the upper bound 32767 15 | mAvail = 2040; 16 | } 17 | ndHeap = Math.round(mAvail*.25); 18 | ndStream = Math.round(mAvail*.25); 19 | ndScratch = Math.round(mAvail*.5); 20 | var ndAllocation = ndHeap + ndStream + ndScratch; 21 | log.debug("Allocating " + ndAllocation + "MB"); 22 | 23 | } else { 24 | // defaults! Likely to run out of RAM 25 | log.debug(memoryLimitInMB + "MB available; 2048MB is recommended."); 26 | ndHeap = 251; 27 | ndStream = 256; 28 | ndScratch = 512; 29 | } 30 | 31 | docx.init({ 32 | ENVIRONMENT: "NODE", 33 | LAZY_INIT: true, 34 | ND_MAX_HEAP_SIZE_MB: ndHeap, 35 | ND_MAX_STREAM_SIZE_MB: ndStream, 36 | ND_MAX_SCRATCH_SIZE_MB: ndScratch 37 | }); 38 | log.debug("Initialised using " + memoryLimitInMB + "MB"); 39 | } 40 | -------------------------------------------------------------------------------- /lambda/helper.js: -------------------------------------------------------------------------------- 1 | var docx = require("@nativedocuments/docx-wasm"); 2 | const Format = require("@nativedocuments/docx-wasm/formats"); 3 | 4 | const log = require('lambda-log'); 5 | 6 | 7 | /** 8 | * Convert the doc/docx to specified output format (eg PDF) 9 | * @param {string} jobId - An identifier for diagnostic purposes (eg filename) 10 | * @param {object} docIn - Input document (a buffer or filename) 11 | * @param {Format} formatOut - Desired output format (eg Format.PDF) 12 | */ 13 | exports.convert = async function (jobId, docIn, formatOut) { 14 | 15 | log.debug(jobId + " await received "); 16 | const engine = await docx.engine(); 17 | try { 18 | // load docx into engine 19 | await engine.load(docIn); 20 | log.debug(jobId + " loaded into docx_api " ); 21 | // now export it 22 | var buffer; 23 | if (formatOut==Format.DOCX) { 24 | buffer=await engine.exportDOCX(); 25 | } else if (formatOut==Format.PDF) { 26 | buffer=await engine.exportPDF(); 27 | } else { 28 | throw new Error("Unsupported output format " + formatOut); 29 | } 30 | // close engine 31 | await engine.close(); 32 | return buffer; 33 | } catch (e) { 34 | await engine.close(); 35 | throw (e); 36 | // if (e) log.debug(e); 37 | // log.error("caught error loading "+srcKey); 38 | } 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /lambda/index.js: -------------------------------------------------------------------------------- 1 | var docx = require("@nativedocuments/docx-wasm"); 2 | var AWS = require('aws-sdk'); 3 | var s3 = new AWS.S3(); 4 | var config = require('./configure'); 5 | var helper = require('./helper'); 6 | 7 | const log = require('lambda-log'); 8 | 9 | const Format = require("@nativedocuments/docx-wasm/formats"); 10 | 11 | // ND_LICENSE_URL, or ND_DEV_ID and ND_DEV_SECRET are read from environment 12 | // If using the S3 trigger, S3_BUCKET_OUTPUT should be set there as well 13 | // See README for more details 14 | 15 | // debug messages? 16 | if (process.env.DEPLOY_ENV !== 'PROD') { 17 | log.options.debug = true; 18 | } 19 | 20 | // The use case for this app is to output PDF. 21 | // But if your use case is binary .doc to docx conversion, 22 | // change this to Format.DOCX 23 | var outputAs = Format.PDF; 24 | 25 | var srcBucket; 26 | var srcKey; 27 | 28 | var dstBucket; 29 | var dstKey; 30 | 31 | var INITIALISED = false; 32 | 33 | 34 | /** 35 | * Lambda which converts an S3 object. 36 | * It can be invoked from an AWS Step Function, 37 | * or by an S3 trigger. 38 | */ 39 | exports.handler = async function(event, context) { 40 | 41 | var isStep = false; 42 | var isSQS = false; 43 | var correlationId; // optional, 44 | if (/* our AWS Step Function */ event.source_bucket ) { 45 | 46 | isStep = true; 47 | 48 | //log.warn(event); 49 | srcBucket = event.source_bucket; 50 | srcKey = event.source_key; 51 | 52 | dstBucket = event.target_bucket; 53 | dstKey = event.target_key; 54 | 55 | } else if (/* Lambda S3 trigger */ event.Records && event.Records[0].eventSource === 'aws:s3') { 56 | 57 | //log.debug("received an S3 event"); 58 | //log.debug(event); 59 | 60 | // Object key may have spaces or unicode non-ASCII characters. 61 | srcBucket = event.Records[0].s3.bucket.name; 62 | srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " ")); 63 | 64 | // Avoid identity conversion, especially srcbucket == dstbucket 65 | // where we'd cause an event loop 66 | if (srcKey.endsWith('docx') && outputAs==Format.DOCX ) { 67 | log.warn('Identity conversion avoided'); 68 | return; 69 | } 70 | 71 | // if dstBucket is undefined, we'll use source bucket. 72 | dstBucket = process.env.S3_BUCKET_OUTPUT; 73 | if (dstBucket === undefined) { 74 | dstBucket = srcBucket; 75 | } 76 | 77 | // dstKey is computed below 78 | if (outputAs==Format.DOCX) { 79 | dstKey = srcKey + ".docx"; 80 | } else if (outputAs==Format.PDF) { 81 | dstKey = srcKey + ".pdf"; 82 | } else { 83 | log.error("Unsupported output format " + outputAs); 84 | return; 85 | } 86 | 87 | } else if (/* SQS trigger */ event.Records && event.Records[0].eventSource === 'aws:sqs') { 88 | 89 | isSQS = true; 90 | 91 | /* It listens to the specific queue configured in Lambda. 92 | * 93 | * You can specify the batch size, but here we assume batch size = 1. 94 | * 95 | { 96 | "Records": [ 97 | { 98 | "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", 99 | "receiptHandle": "MessageReceiptHandle", 100 | "body": { 101 | "source_bucket": "YOUR_INPUT_BUCKET_NAME", 102 | "source_key": "YOUR.docx", 103 | "target_bucket": "YOUR_OUTPUT_BUCKET_NAME", 104 | "target_key": "YOUR.pdf" 105 | }, 106 | "attributes": { 107 | "ApproximateReceiveCount": "1", 108 | "SentTimestamp": "1523232000000", 109 | "SenderId": "123456789012", 110 | "ApproximateFirstReceiveTimestamp": "1523232000001" 111 | }, 112 | "messageAttributes": { "correlationId": "foo123"}, 113 | "md5OfBody": "7b270e59b47ff90a553787216d55d91d", 114 | "eventSource": "aws:sqs", 115 | "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:MyQueue", 116 | "awsRegion": "us-west-2" 117 | } 118 | ] 119 | } 120 | * 121 | */ 122 | 123 | const { body } = event.Records[0]; 124 | log.debug(body); 125 | 126 | srcBucket = body.source_bucket; 127 | srcKey = body.source_key; 128 | 129 | dstBucket = body.target_bucket; 130 | dstKey = body.target_key; 131 | 132 | correlationId = event.Records[0].messageAttributes.correlationId; 133 | //.debug(correlationId); 134 | 135 | 136 | } else { 137 | // see https://stackoverflow.com/questions/41814750/how-to-know-event-souce-of-lambda-function-in-itself 138 | // for other event sources 139 | 140 | // Modify to suit your usecase 141 | log.warn("Unexpected invocation"); 142 | log.warn(event); 143 | return; 144 | } 145 | 146 | if (!srcKey.endsWith('doc') && !srcKey.endsWith('docx') ) { 147 | log.warn('Unsupported file type ' + srcKey); 148 | return; // TODO: if step function, return error? 149 | } 150 | if (srcKey.endsWith("/")) { 151 | // assume this is a folder; event probably triggered by copy/pasting a folder 152 | log.debug("is folder; returning"); 153 | return; 154 | } 155 | 156 | // Output input URL for ease of inspection 157 | log.info("https://s3.console.aws.amazon.com/s3/object/" + srcBucket + "/" + srcKey); 158 | 159 | 160 | // Compute mimeType 161 | var mimeType; 162 | if (outputAs==Format.DOCX) { 163 | mimeType = Format.DOC.toString(); 164 | } else if (outputAs==Format.PDF) { 165 | mimeType = Format.PDF.toString(); 166 | } else { 167 | log.error("Unsupported output format " + outputAs); 168 | return; 169 | } 170 | 171 | // initialise engine. 172 | // This is inside the handler since we need to read memoryLimitInMB from context 173 | if (!INITIALISED) { 174 | try { 175 | config.init(context.memoryLimitInMB); 176 | INITIALISED = true; 177 | } catch (e) { 178 | log.error(e); 179 | return; 180 | } 181 | } 182 | var sqsQueueUrl = process.env.SQS_WRITE_QUEUE_URL; 183 | 184 | // Actually execute the steps 185 | var data; 186 | try { 187 | // get the docx 188 | data = await s3.getObject( {Bucket: srcBucket, Key: srcKey}).promise(); 189 | 190 | // convert it 191 | var output = await helper.convert(srcKey, data.Body, outputAs ); 192 | 193 | // save the result 194 | log.debug("uploading to s3 " + dstBucket); 195 | await s3.putObject({ 196 | Bucket: dstBucket, 197 | Key: dstKey, 198 | Body: new Buffer(output) /* arrayBuffer to Buffer */, 199 | ContentType: mimeType 200 | }).promise(); 201 | log.info('RESULT: Success ' + dstKey); /* Log analysis regex matching */ 202 | 203 | //log.info(sqsQueueUrl); 204 | if (sqsQueueUrl) { 205 | // send SQS message 206 | //log.info("write to sqs"); 207 | 208 | // Create an SQS service object 209 | var sqs = new AWS.SQS({apiVersion: '2012-11-05'}); 210 | 211 | var payload = { 212 | bucket: dstBucket, 213 | key: dstKey 214 | }; 215 | 216 | var params = { 217 | MessageBody: JSON.stringify(payload), 218 | QueueUrl: sqsQueueUrl 219 | }; 220 | 221 | if (correlationId) { 222 | params = Object.assign( { 223 | MessageAttributes: { "correlationId": { 224 | DataType: "String", 225 | StringValue: correlationId 226 | } 227 | } 228 | }, params); 229 | } 230 | 231 | log.info(params); 232 | 233 | sqs.sendMessage(params, function(err, data) { 234 | if (err) { 235 | log.error("Error", err); 236 | } else { 237 | //log.debug("Success", data.MessageId); 238 | } 239 | }); 240 | } 241 | 242 | // Return a result (useful where invoked from a step function) 243 | return { 'RESULT' : 'Success', "key" : dstKey }; 244 | 245 | } catch (e) { 246 | 247 | //const msg = "" + e; 248 | if (e) log.error(e); 249 | 250 | if (isStep) { 251 | log.error("RESULT: Failed " + dstKey ); /* Log analysis regex matching */ 252 | // Return a result (step function can catch this) 253 | throw e; 254 | } 255 | if (sqsQueueUrl) { 256 | // TODO: write SQS message on failure? 257 | } 258 | 259 | /* For S3 trigger, broken documents saved to dstBucket/BROKEN 260 | To get help, please note the contents of the assertion, 261 | together with the document which caused it. 262 | */ 263 | 264 | // save broken documents to dstBucket/BROKEN 265 | /* unless */ 266 | if (dstBucket == srcBucket) /* to avoid repetitively processing the same document */ { 267 | log.error("RESULT: Failed " + srcKey); 268 | log.debug("cowardly refusing to write broken document to srcBucket!"); 269 | return; 270 | } 271 | var ext = srcKey.substr(srcKey.lastIndexOf('.') + 1); 272 | var mimeType; 273 | if (ext=="docx") { 274 | mimeType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; 275 | } else if (ext=="doc") { 276 | mimeType = Format.DOC.toString(); 277 | } else { 278 | mimeType = "application/octet-stream"; 279 | } 280 | 281 | dstKey = "BROKEN/" + srcKey + "-" + (new Date).getTime() + "." + ext; 282 | log.error("RESULT: Failed " + dstKey ); /* Log analysis regex matching */ 283 | 284 | // save this bug doc 285 | try { 286 | await s3.putObject({ 287 | Bucket: dstBucket, 288 | Key: dstKey, 289 | Body: new Buffer(data.Body) /* arrayBuffer to Buffer */, 290 | ContentType: mimeType 291 | }).promise(); 292 | } catch (putErr) { 293 | log.error(putErr); 294 | log.error("Problem saving bug doc " + dstKey ); 295 | } 296 | 297 | } 298 | }; 299 | 300 | -------------------------------------------------------------------------------- /lambda/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docx-to-pdf-on-AWS-Lambda", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@nativedocuments/docx-wasm": { 8 | "version": "2.2.8-1547344486", 9 | "resolved": "https://registry.npmjs.org/@nativedocuments/docx-wasm/-/docx-wasm-2.2.8-1547344486.tgz", 10 | "integrity": "sha512-LkfK0bWx6raazfKLqVsHi1vYbGCyUM5fZCz6txjYa4NK/fOelIErmmKwRlViqO7B91LqlBr0qLNihsv21lhphQ==", 11 | "requires": { 12 | "@nativedocuments/fontpool": "1.0.0-v78032863279A6C0FF0C79E4D0E9A6BD0" 13 | } 14 | }, 15 | "@nativedocuments/fontpool": { 16 | "version": "1.0.0-v78032863279A6C0FF0C79E4D0E9A6BD0", 17 | "resolved": "https://registry.npmjs.org/@nativedocuments/fontpool/-/fontpool-1.0.0-v78032863279A6C0FF0C79E4D0E9A6BD0.tgz", 18 | "integrity": "sha512-Y6ESmuRXmQfCQaPDSlmPtaRKEY/LkWkGRNI7oasf8rHmZDTVbo/ty3aNWYVGtwjQ8FEJKeojLd9irSb3+XBbCg==" 19 | }, 20 | "assertion-error": { 21 | "version": "1.1.0", 22 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 23 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 24 | "dev": true 25 | }, 26 | "aws-sdk": { 27 | "version": "2.395.0", 28 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.395.0.tgz", 29 | "integrity": "sha512-ldTTjctniZT4E2lq2z3D8Y2u+vpkp+laoEnDkXgjKXTKbiJ0QEtfWsUdx/IQ7awCt8stoxyqZK47DJOxIbRNoA==", 30 | "requires": { 31 | "buffer": "4.9.1", 32 | "events": "1.1.1", 33 | "ieee754": "1.1.8", 34 | "jmespath": "0.15.0", 35 | "querystring": "0.2.0", 36 | "sax": "1.2.1", 37 | "url": "0.10.3", 38 | "uuid": "3.3.2", 39 | "xml2js": "0.4.19" 40 | } 41 | }, 42 | "balanced-match": { 43 | "version": "1.0.0", 44 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 45 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 46 | "dev": true 47 | }, 48 | "base64-js": { 49 | "version": "1.3.0", 50 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", 51 | "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" 52 | }, 53 | "brace-expansion": { 54 | "version": "1.1.11", 55 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 56 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 57 | "dev": true, 58 | "requires": { 59 | "balanced-match": "^1.0.0", 60 | "concat-map": "0.0.1" 61 | } 62 | }, 63 | "browser-stdout": { 64 | "version": "1.3.1", 65 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 66 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 67 | "dev": true 68 | }, 69 | "buffer": { 70 | "version": "4.9.1", 71 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", 72 | "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", 73 | "requires": { 74 | "base64-js": "^1.0.2", 75 | "ieee754": "^1.1.4", 76 | "isarray": "^1.0.0" 77 | } 78 | }, 79 | "chai": { 80 | "version": "4.2.0", 81 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", 82 | "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", 83 | "dev": true, 84 | "requires": { 85 | "assertion-error": "^1.1.0", 86 | "check-error": "^1.0.2", 87 | "deep-eql": "^3.0.1", 88 | "get-func-name": "^2.0.0", 89 | "pathval": "^1.1.0", 90 | "type-detect": "^4.0.5" 91 | } 92 | }, 93 | "check-error": { 94 | "version": "1.0.2", 95 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 96 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 97 | "dev": true 98 | }, 99 | "commander": { 100 | "version": "2.15.1", 101 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 102 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 103 | "dev": true 104 | }, 105 | "concat-map": { 106 | "version": "0.0.1", 107 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 108 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 109 | "dev": true 110 | }, 111 | "debug": { 112 | "version": "3.1.0", 113 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 114 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 115 | "dev": true, 116 | "requires": { 117 | "ms": "2.0.0" 118 | } 119 | }, 120 | "deep-eql": { 121 | "version": "3.0.1", 122 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 123 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 124 | "dev": true, 125 | "requires": { 126 | "type-detect": "^4.0.0" 127 | } 128 | }, 129 | "diff": { 130 | "version": "3.5.0", 131 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 132 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 133 | "dev": true 134 | }, 135 | "escape-string-regexp": { 136 | "version": "1.0.5", 137 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 138 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 139 | "dev": true 140 | }, 141 | "events": { 142 | "version": "1.1.1", 143 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 144 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" 145 | }, 146 | "fs.realpath": { 147 | "version": "1.0.0", 148 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 149 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 150 | "dev": true 151 | }, 152 | "get-func-name": { 153 | "version": "2.0.0", 154 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 155 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 156 | "dev": true 157 | }, 158 | "glob": { 159 | "version": "7.1.2", 160 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 161 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 162 | "dev": true, 163 | "requires": { 164 | "fs.realpath": "^1.0.0", 165 | "inflight": "^1.0.4", 166 | "inherits": "2", 167 | "minimatch": "^3.0.4", 168 | "once": "^1.3.0", 169 | "path-is-absolute": "^1.0.0" 170 | } 171 | }, 172 | "growl": { 173 | "version": "1.10.5", 174 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 175 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 176 | "dev": true 177 | }, 178 | "has-flag": { 179 | "version": "3.0.0", 180 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 181 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 182 | "dev": true 183 | }, 184 | "he": { 185 | "version": "1.1.1", 186 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 187 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 188 | "dev": true 189 | }, 190 | "ieee754": { 191 | "version": "1.1.8", 192 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", 193 | "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" 194 | }, 195 | "inflight": { 196 | "version": "1.0.6", 197 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 198 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 199 | "dev": true, 200 | "requires": { 201 | "once": "^1.3.0", 202 | "wrappy": "1" 203 | } 204 | }, 205 | "inherits": { 206 | "version": "2.0.3", 207 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 208 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 209 | "dev": true 210 | }, 211 | "isarray": { 212 | "version": "1.0.0", 213 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 214 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 215 | }, 216 | "jmespath": { 217 | "version": "0.15.0", 218 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 219 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" 220 | }, 221 | "json-stringify-safe": { 222 | "version": "5.0.1", 223 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 224 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 225 | }, 226 | "lambda-log": { 227 | "version": "2.1.0", 228 | "resolved": "https://registry.npmjs.org/lambda-log/-/lambda-log-2.1.0.tgz", 229 | "integrity": "sha512-Ins/QUdYkV7LZEyAV2ndCVPdK7KXzsf/1I542Exi73Xez4k7SaqfagNYptmdtJ3kx8mkWrWla6eb5G5g4mM+aQ==", 230 | "requires": { 231 | "json-stringify-safe": "^5.0.1" 232 | } 233 | }, 234 | "minimatch": { 235 | "version": "3.0.4", 236 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 237 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 238 | "dev": true, 239 | "requires": { 240 | "brace-expansion": "^1.1.7" 241 | } 242 | }, 243 | "minimist": { 244 | "version": "0.0.8", 245 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 246 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 247 | "dev": true 248 | }, 249 | "mkdirp": { 250 | "version": "0.5.1", 251 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 252 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 253 | "dev": true, 254 | "requires": { 255 | "minimist": "0.0.8" 256 | } 257 | }, 258 | "mocha": { 259 | "version": "5.2.0", 260 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 261 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 262 | "dev": true, 263 | "requires": { 264 | "browser-stdout": "1.3.1", 265 | "commander": "2.15.1", 266 | "debug": "3.1.0", 267 | "diff": "3.5.0", 268 | "escape-string-regexp": "1.0.5", 269 | "glob": "7.1.2", 270 | "growl": "1.10.5", 271 | "he": "1.1.1", 272 | "minimatch": "3.0.4", 273 | "mkdirp": "0.5.1", 274 | "supports-color": "5.4.0" 275 | } 276 | }, 277 | "ms": { 278 | "version": "2.0.0", 279 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 280 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 281 | "dev": true 282 | }, 283 | "once": { 284 | "version": "1.4.0", 285 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 286 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 287 | "dev": true, 288 | "requires": { 289 | "wrappy": "1" 290 | } 291 | }, 292 | "path-is-absolute": { 293 | "version": "1.0.1", 294 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 295 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 296 | "dev": true 297 | }, 298 | "pathval": { 299 | "version": "1.1.0", 300 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 301 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 302 | "dev": true 303 | }, 304 | "punycode": { 305 | "version": "1.3.2", 306 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 307 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 308 | }, 309 | "querystring": { 310 | "version": "0.2.0", 311 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 312 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 313 | }, 314 | "sax": { 315 | "version": "1.2.1", 316 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 317 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" 318 | }, 319 | "supports-color": { 320 | "version": "5.4.0", 321 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 322 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 323 | "dev": true, 324 | "requires": { 325 | "has-flag": "^3.0.0" 326 | } 327 | }, 328 | "type-detect": { 329 | "version": "4.0.8", 330 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 331 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 332 | "dev": true 333 | }, 334 | "url": { 335 | "version": "0.10.3", 336 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 337 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 338 | "requires": { 339 | "punycode": "1.3.2", 340 | "querystring": "0.2.0" 341 | } 342 | }, 343 | "uuid": { 344 | "version": "3.3.2", 345 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 346 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 347 | }, 348 | "wrappy": { 349 | "version": "1.0.2", 350 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 351 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 352 | "dev": true 353 | }, 354 | "xml2js": { 355 | "version": "0.4.19", 356 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", 357 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", 358 | "requires": { 359 | "sax": ">=0.6.0", 360 | "xmlbuilder": "~9.0.1" 361 | } 362 | }, 363 | "xmlbuilder": { 364 | "version": "9.0.7", 365 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 366 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" 367 | } 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docx-to-pdf-on-AWS-Lambda", 3 | "version": "1.0.0", 4 | "description": "Microsoft Word doc/docx to PDF conversion, using S3 buckets", 5 | "main": "index.js", 6 | "repository": "https://github.com/NativeDocuments/docx-to-pdf-on-AWS-Lambda", 7 | "author": "Native Documents", 8 | "license": "MIT", 9 | "scripts": { 10 | "test": "tests/unit/" 11 | }, 12 | "devDependencies": { 13 | "chai": "^4.1.2", 14 | "mocha": "^5.1.1" 15 | }, 16 | "dependencies": { 17 | "aws-sdk": "^2.382.0", 18 | "lambda-log": "^2.1.0", 19 | "@nativedocuments/docx-wasm": "2.2.8-1547344486" 20 | } 21 | } 22 | --------------------------------------------------------------------------------