@unchainedshop/plugins 4.7.2 → 4.8.0

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 (184) hide show
  1. package/README.md +2 -0
  2. package/lib/delivery/post.d.ts +1 -0
  3. package/lib/delivery/post.js +23 -0
  4. package/lib/delivery/send-message.d.ts +1 -0
  5. package/lib/delivery/send-message.js +40 -0
  6. package/lib/delivery/stores.d.ts +1 -0
  7. package/lib/delivery/stores.js +43 -0
  8. package/lib/enrollments/licensed.d.ts +4 -0
  9. package/lib/enrollments/licensed.js +52 -0
  10. package/lib/events/node-event-emitter.d.ts +1 -3
  11. package/lib/events/node-event-emitter.js +3 -3
  12. package/lib/files/gridfs/adapter.d.ts +2 -2
  13. package/lib/files/gridfs/adapter.js +5 -2
  14. package/lib/files/gridfs/handler-express.d.ts +9 -0
  15. package/lib/files/gridfs/handler-express.js +110 -0
  16. package/lib/files/gridfs/handler-fastify.d.ts +3 -0
  17. package/lib/files/gridfs/handler-fastify.js +138 -0
  18. package/lib/files/gridfs/index.d.ts +4 -5
  19. package/lib/files/gridfs/index.js +3 -30
  20. package/lib/files/minio/handler-express.d.ts +2 -0
  21. package/lib/files/minio/handler-express.js +36 -0
  22. package/lib/files/minio/handler-fastify.d.ts +3 -0
  23. package/lib/files/minio/handler-fastify.js +40 -0
  24. package/lib/files/minio/index.d.ts +5 -4
  25. package/lib/files/minio/index.js +157 -18
  26. package/lib/filters/local-search.d.ts +3 -0
  27. package/lib/filters/local-search.js +66 -0
  28. package/lib/filters/strict-equal.d.ts +1 -0
  29. package/lib/filters/strict-equal.js +24 -0
  30. package/lib/payment/apple-iap/adapter.d.ts +1 -2
  31. package/lib/payment/apple-iap/adapter.js +3 -2
  32. package/lib/payment/apple-iap/handler-express.d.ts +2 -0
  33. package/lib/payment/apple-iap/handler-express.js +103 -0
  34. package/lib/payment/apple-iap/handler-fastify.d.ts +3 -0
  35. package/lib/payment/apple-iap/handler-fastify.js +102 -0
  36. package/lib/payment/apple-iap/index.d.ts +4 -6
  37. package/lib/payment/apple-iap/index.js +3 -28
  38. package/lib/payment/braintree.d.ts +1 -0
  39. package/lib/payment/braintree.js +138 -0
  40. package/lib/payment/cryptopay/handler-express.d.ts +2 -0
  41. package/lib/payment/cryptopay/handler-express.js +19 -0
  42. package/lib/payment/cryptopay/handler-fastify.d.ts +3 -0
  43. package/lib/payment/cryptopay/handler-fastify.js +17 -0
  44. package/lib/payment/cryptopay/index.d.ts +4 -5
  45. package/lib/payment/cryptopay/index.js +3 -31
  46. package/lib/payment/cryptopay/plugin.d.ts +3 -0
  47. package/lib/payment/cryptopay/plugin.js +159 -0
  48. package/lib/payment/datatrans-v2/handler-express.d.ts +1 -0
  49. package/lib/payment/datatrans-v2/handler-express.js +71 -0
  50. package/lib/payment/datatrans-v2/handler-fastify.d.ts +2 -0
  51. package/lib/payment/datatrans-v2/handler-fastify.js +71 -0
  52. package/lib/payment/datatrans-v2/index.d.ts +1 -4
  53. package/lib/payment/datatrans-v2/index.js +400 -17
  54. package/lib/payment/invoice-prepaid.d.ts +1 -0
  55. package/lib/payment/invoice-prepaid.js +30 -0
  56. package/lib/payment/invoice.d.ts +1 -0
  57. package/lib/payment/invoice.js +27 -0
  58. package/lib/payment/paypal-checkout.d.ts +1 -0
  59. package/lib/payment/paypal-checkout.js +87 -0
  60. package/lib/payment/payrexx/handler-express.d.ts +1 -0
  61. package/lib/payment/payrexx/handler-express.js +85 -0
  62. package/lib/payment/payrexx/handler-fastify.d.ts +2 -0
  63. package/lib/payment/payrexx/handler-fastify.js +91 -0
  64. package/lib/payment/payrexx/index.d.ts +1 -4
  65. package/lib/payment/payrexx/index.js +158 -19
  66. package/lib/payment/postfinance-checkout/handler-express.d.ts +1 -0
  67. package/lib/payment/postfinance-checkout/handler-express.js +37 -0
  68. package/lib/payment/postfinance-checkout/handler-fastify.d.ts +2 -0
  69. package/lib/payment/postfinance-checkout/handler-fastify.js +40 -0
  70. package/lib/payment/postfinance-checkout/index.d.ts +0 -5
  71. package/lib/payment/postfinance-checkout/index.js +186 -25
  72. package/lib/payment/saferpay/adapter.js +2 -1
  73. package/lib/payment/saferpay/handler-express.d.ts +1 -0
  74. package/lib/payment/saferpay/handler-express.js +59 -0
  75. package/lib/payment/saferpay/handler-fastify.d.ts +2 -0
  76. package/lib/payment/saferpay/handler-fastify.js +62 -0
  77. package/lib/payment/saferpay/index.d.ts +4 -5
  78. package/lib/payment/saferpay/index.js +3 -29
  79. package/lib/payment/stripe/handler-express.d.ts +5 -0
  80. package/lib/payment/stripe/handler-express.js +121 -0
  81. package/lib/payment/stripe/handler-fastify.d.ts +6 -0
  82. package/lib/payment/stripe/handler-fastify.js +131 -0
  83. package/lib/payment/stripe/index.d.ts +1 -4
  84. package/lib/payment/stripe/index.js +116 -22
  85. package/lib/payment/stripe/stripe.js +4 -1
  86. package/lib/presets/all-express.d.ts +6 -0
  87. package/lib/presets/all-express.js +26 -0
  88. package/lib/presets/all-fastify.d.ts +6 -0
  89. package/lib/presets/all-fastify.js +49 -0
  90. package/lib/presets/all.d.ts +70 -1
  91. package/lib/presets/all.js +27 -41
  92. package/lib/presets/base-express.d.ts +2 -0
  93. package/lib/presets/base-express.js +5 -0
  94. package/lib/presets/base-fastify.d.ts +3 -0
  95. package/lib/presets/base-fastify.js +16 -0
  96. package/lib/presets/base.d.ts +25 -1
  97. package/lib/presets/base.js +25 -56
  98. package/lib/presets/countries/ch.d.ts +2 -1
  99. package/lib/presets/countries/ch.js +2 -7
  100. package/lib/presets/crypto-express.d.ts +3 -0
  101. package/lib/presets/crypto-express.js +8 -0
  102. package/lib/presets/crypto-fastify.d.ts +3 -0
  103. package/lib/presets/crypto-fastify.js +11 -0
  104. package/lib/presets/crypto.d.ts +7 -3
  105. package/lib/presets/crypto.js +7 -18
  106. package/lib/pricing/delivery-swiss-tax.d.ts +2 -0
  107. package/lib/pricing/delivery-swiss-tax.js +62 -0
  108. package/lib/pricing/discount-100-off.d.ts +2 -0
  109. package/lib/pricing/discount-100-off.js +30 -0
  110. package/lib/pricing/discount-half-price-manual.d.ts +2 -0
  111. package/lib/pricing/discount-half-price-manual.js +30 -0
  112. package/lib/pricing/discount-half-price.d.ts +2 -0
  113. package/lib/pricing/discount-half-price.js +33 -0
  114. package/lib/pricing/free-delivery.d.ts +2 -0
  115. package/lib/pricing/free-delivery.js +27 -0
  116. package/lib/pricing/free-payment.d.ts +2 -0
  117. package/lib/pricing/free-payment.js +27 -0
  118. package/lib/pricing/order-delivery.d.ts +2 -0
  119. package/lib/pricing/order-delivery.js +33 -0
  120. package/lib/pricing/order-discount.d.ts +2 -0
  121. package/lib/pricing/order-discount.js +74 -0
  122. package/lib/pricing/order-items-discount.d.ts +2 -0
  123. package/lib/pricing/order-items-discount.js +50 -0
  124. package/lib/pricing/order-items.d.ts +2 -0
  125. package/lib/pricing/order-items.js +43 -0
  126. package/lib/pricing/order-payment.d.ts +2 -0
  127. package/lib/pricing/order-payment.js +33 -0
  128. package/lib/pricing/order-round.d.ts +10 -0
  129. package/lib/pricing/order-round.js +135 -0
  130. package/lib/pricing/product-catalog-price-options.d.ts +2 -0
  131. package/lib/pricing/product-catalog-price-options.js +51 -0
  132. package/lib/pricing/product-catalog-price.d.ts +2 -0
  133. package/lib/pricing/product-catalog-price.js +60 -0
  134. package/lib/pricing/product-discount.d.ts +2 -0
  135. package/lib/pricing/product-discount.js +69 -0
  136. package/lib/pricing/product-price-rateconversion.d.ts +2 -0
  137. package/lib/pricing/product-price-rateconversion.js +53 -0
  138. package/lib/pricing/product-round.d.ts +10 -0
  139. package/lib/pricing/product-round.js +44 -0
  140. package/lib/pricing/product-swiss-tax.d.ts +2 -0
  141. package/lib/pricing/product-swiss-tax.js +76 -0
  142. package/lib/quotations/manual.d.ts +1 -0
  143. package/lib/quotations/manual.js +20 -0
  144. package/lib/warehousing/eth-minter.d.ts +1 -0
  145. package/lib/warehousing/eth-minter.js +120 -0
  146. package/lib/warehousing/store.d.ts +1 -0
  147. package/lib/warehousing/store.js +34 -0
  148. package/lib/worker/budgetsms.d.ts +9 -0
  149. package/lib/worker/budgetsms.js +169 -0
  150. package/lib/worker/bulk-export.d.ts +2 -0
  151. package/lib/worker/bulk-export.js +64 -0
  152. package/lib/worker/bulk-import.d.ts +2 -0
  153. package/lib/worker/bulk-import.js +65 -0
  154. package/lib/worker/bulkgate.d.ts +1 -0
  155. package/lib/worker/bulkgate.js +91 -0
  156. package/lib/worker/email.d.ts +1 -0
  157. package/lib/worker/email.js +136 -0
  158. package/lib/worker/enrollment-order-generator.d.ts +1 -0
  159. package/lib/worker/enrollment-order-generator.js +77 -0
  160. package/lib/worker/error-notifications.d.ts +9 -0
  161. package/lib/worker/error-notifications.js +61 -0
  162. package/lib/worker/export-token.d.ts +4 -0
  163. package/lib/worker/export-token.js +26 -0
  164. package/lib/worker/external.d.ts +2 -0
  165. package/lib/worker/external.js +13 -0
  166. package/lib/worker/heartbeat.d.ts +1 -0
  167. package/lib/worker/heartbeat.js +26 -0
  168. package/lib/worker/http-request.d.ts +8 -0
  169. package/lib/worker/http-request.js +56 -0
  170. package/lib/worker/message.d.ts +10 -0
  171. package/lib/worker/message.js +42 -0
  172. package/lib/worker/push-notification.d.ts +1 -0
  173. package/lib/worker/push-notification.js +87 -0
  174. package/lib/worker/twilio.d.ts +1 -0
  175. package/lib/worker/twilio.js +59 -0
  176. package/lib/worker/update-coinbase-rates.d.ts +1 -0
  177. package/lib/worker/update-coinbase-rates.js +59 -0
  178. package/lib/worker/update-ecb-rates.d.ts +1 -0
  179. package/lib/worker/update-ecb-rates.js +92 -0
  180. package/lib/worker/update-token-ownership.d.ts +3 -0
  181. package/lib/worker/update-token-ownership.js +57 -0
  182. package/lib/worker/zombie-killer.d.ts +13 -0
  183. package/lib/worker/zombie-killer.js +91 -0
  184. package/package.json +10 -15
package/README.md CHANGED
@@ -22,6 +22,7 @@ npm install @unchainedshop/plugins
22
22
  | Stripe | `payment/stripe` | Stripe payment integration |
23
23
  | Datatrans V2 | `payment/datatrans-v2` | Datatrans payment gateway |
24
24
  | Saferpay | `payment/saferpay` | Saferpay payment gateway |
25
+ | PayPal Checkout | `payment/paypal-checkout` | PayPal Checkout integration |
25
26
  | Braintree | `payment/braintree` | Braintree payments |
26
27
  | Payrexx | `payment/payrexx` | Payrexx payment gateway |
27
28
  | Apple IAP | `payment/apple-iap` | Apple In-App Purchase |
@@ -133,6 +134,7 @@ All payment plugins implement secure tokenization patterns for PCI DSS SAQ-A eli
133
134
  | Stripe | PaymentIntent/SetupIntent tokenization |
134
135
  | Datatrans | Secure Fields with HMAC-SHA-256 signatures |
135
136
  | Saferpay | Redirect with SHA-256 transaction signatures |
137
+ | PayPal | Order ID references |
136
138
  | Braintree | Client SDK tokenization |
137
139
  | Cryptopay | BIP-32 HD wallet address derivation |
138
140
 
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { DeliveryAdapter, DeliveryDirector } from '@unchainedshop/core';
2
+ import { DeliveryProviderType } from '@unchainedshop/core-delivery';
3
+ const Post = {
4
+ ...DeliveryAdapter,
5
+ key: 'shop.unchained.post',
6
+ label: 'Manual',
7
+ version: '1.0.0',
8
+ typeSupported: (type) => {
9
+ return type === DeliveryProviderType.SHIPPING;
10
+ },
11
+ actions: (config, context) => {
12
+ return {
13
+ ...DeliveryAdapter.actions(config, context),
14
+ isActive: () => {
15
+ return true;
16
+ },
17
+ configurationError: () => {
18
+ return null;
19
+ },
20
+ };
21
+ },
22
+ };
23
+ DeliveryDirector.registerAdapter(Post);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { DeliveryAdapter, DeliveryDirector } from '@unchainedshop/core';
2
+ import { DeliveryProviderType } from '@unchainedshop/core-delivery';
3
+ const SendMessage = {
4
+ ...DeliveryAdapter,
5
+ key: 'shop.unchained.delivery.send-message',
6
+ label: 'Forward via Messaging',
7
+ version: '1.0.0',
8
+ initialConfiguration: [
9
+ { key: 'from', value: '' },
10
+ { key: 'to', value: '' },
11
+ { key: 'cc', value: '' },
12
+ ],
13
+ typeSupported: (type) => {
14
+ return type === DeliveryProviderType.SHIPPING;
15
+ },
16
+ actions: (config, context) => {
17
+ return {
18
+ ...DeliveryAdapter.actions(config, context),
19
+ isActive: () => {
20
+ return true;
21
+ },
22
+ configurationError: () => {
23
+ return null;
24
+ },
25
+ send: async () => {
26
+ const { modules, order } = context;
27
+ return modules.worker.addWork({
28
+ type: 'MESSAGE',
29
+ retries: 0,
30
+ input: {
31
+ template: 'DELIVERY',
32
+ orderId: order._id,
33
+ config,
34
+ },
35
+ });
36
+ },
37
+ };
38
+ },
39
+ };
40
+ DeliveryDirector.registerAdapter(SendMessage);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { DeliveryAdapter, DeliveryDirector } from '@unchainedshop/core';
2
+ import { DeliveryProviderType } from '@unchainedshop/core-delivery';
3
+ const PickMup = {
4
+ ...DeliveryAdapter,
5
+ key: 'shop.unchained.stores',
6
+ label: 'Pre-configured Stores',
7
+ version: '1.0.0',
8
+ typeSupported: (type) => {
9
+ return type === DeliveryProviderType.PICKUP;
10
+ },
11
+ actions: (config, context) => {
12
+ const getStores = () => {
13
+ const storesItem = config.find((item) => item.key === 'stores');
14
+ if (!storesItem)
15
+ return [];
16
+ try {
17
+ return typeof storesItem.value === 'string' ? JSON.parse(storesItem.value) : storesItem.value;
18
+ }
19
+ catch {
20
+ return storesItem?.value ?? [];
21
+ }
22
+ };
23
+ return {
24
+ ...DeliveryAdapter.actions(config, context),
25
+ isActive() {
26
+ return true;
27
+ },
28
+ isAutoReleaseAllowed() {
29
+ return false;
30
+ },
31
+ configurationError() {
32
+ return null;
33
+ },
34
+ pickUpLocationById: async (id) => {
35
+ return getStores().find((store) => store._id === id);
36
+ },
37
+ pickUpLocations: async () => {
38
+ return getStores();
39
+ },
40
+ };
41
+ },
42
+ };
43
+ DeliveryDirector.registerAdapter(PickMup);
@@ -0,0 +1,4 @@
1
+ export declare const rangeMatcher: (date?: Date) => ({ start, end }: {
2
+ start: any;
3
+ end: any;
4
+ }) => boolean;
@@ -0,0 +1,52 @@
1
+ import { EnrollmentDirector, EnrollmentAdapter } from '@unchainedshop/core';
2
+ export const rangeMatcher = (date = new Date()) => {
3
+ const timestamp = date.getTime();
4
+ return ({ start, end }) => {
5
+ const startTimestamp = new Date(start).getTime();
6
+ const endTimestamp = new Date(end).getTime();
7
+ return startTimestamp <= timestamp && endTimestamp >= timestamp;
8
+ };
9
+ };
10
+ const LicensedEnrollments = {
11
+ ...EnrollmentAdapter,
12
+ key: 'shop.unchained.enrollments.licensed',
13
+ version: '1.0.0',
14
+ label: 'Simple Licensed Enrollments',
15
+ isActivatedFor: (productPlan) => {
16
+ return productPlan?.usageCalculationType === 'LICENSED';
17
+ },
18
+ actions: (params) => {
19
+ const { enrollment } = params;
20
+ return {
21
+ ...EnrollmentAdapter.actions(params),
22
+ isValidForActivation: async () => {
23
+ const periods = enrollment?.periods || [];
24
+ return periods.findIndex(rangeMatcher()) !== -1;
25
+ },
26
+ isOverdue: async () => {
27
+ return false;
28
+ },
29
+ configurationForOrder: async (context) => {
30
+ const { period } = context;
31
+ const beginningOfPeriod = period.start.getTime() <= new Date().getTime();
32
+ if (!enrollment)
33
+ throw new Error('Enrollment missing in context');
34
+ if (beginningOfPeriod) {
35
+ return {
36
+ period,
37
+ orderContext: {},
38
+ orderPositionTemplates: [
39
+ {
40
+ quantity: 1,
41
+ productId: enrollment.productId,
42
+ originalProductId: enrollment.productId,
43
+ },
44
+ ],
45
+ };
46
+ }
47
+ return null;
48
+ },
49
+ };
50
+ },
51
+ };
52
+ EnrollmentDirector.registerAdapter(LicensedEnrollments);
@@ -1,3 +1 @@
1
- import { type EmitAdapter } from '@unchainedshop/events';
2
- export declare const NodeEventEmitter: () => EmitAdapter;
3
- export default NodeEventEmitter;
1
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import {} from '@unchainedshop/events';
3
- export const NodeEventEmitter = () => {
2
+ import { setEmitAdapter } from '@unchainedshop/events';
3
+ const NodeEventEmitter = () => {
4
4
  const eventEmitter = new EventEmitter();
5
5
  return {
6
6
  publish: (eventName, payload) => {
@@ -11,4 +11,4 @@ export const NodeEventEmitter = () => {
11
11
  },
12
12
  };
13
13
  };
14
- export default NodeEventEmitter;
14
+ setEmitAdapter(NodeEventEmitter());
@@ -1,6 +1,6 @@
1
- import { type IFileAdapter } from '@unchainedshop/core-files';
1
+ import { type IFileAdapter } from '@unchainedshop/file-upload';
2
2
  import { type UnchainedCore } from '@unchainedshop/core';
3
- import { type GridFSFileUploadsModule } from './module.ts';
3
+ import { type GridFSFileUploadsModule } from './index.ts';
4
4
  export declare const GridFSAdapter: IFileAdapter<UnchainedCore & {
5
5
  modules: GridFSFileUploadsModule;
6
6
  }>;
@@ -1,10 +1,12 @@
1
1
  import { Readable, PassThrough } from 'node:stream';
2
2
  import { finished, pipeline } from 'node:stream/promises';
3
3
  import mime from 'mime/lite';
4
- import { FileAdapter, buildHashedFilename, resolveExpirationDate, filesSettings, } from '@unchainedshop/core-files';
4
+ import { FileAdapter, FileDirector, buildHashedFilename, resolveExpirationDate, } from '@unchainedshop/file-upload';
5
+ import {} from '@unchainedshop/file-upload';
5
6
  import sign from "./sign.js";
7
+ import { filesSettings } from '@unchainedshop/core-files';
6
8
  import {} from '@unchainedshop/core';
7
- import {} from "./module.js";
9
+ import {} from "./index.js";
8
10
  const { GRIDFS_PUT_SERVER_PATH = '/gridfs' } = process.env;
9
11
  const bufferToStream = (buffer) => {
10
12
  const stream = new Readable();
@@ -111,3 +113,4 @@ export const GridFSAdapter = {
111
113
  await Promise.all(files.map(async ({ _id, path }) => modules.gridfsFileUploads.removeFileFromBucket(path, _id)));
112
114
  },
113
115
  };
116
+ FileDirector.registerAdapter(GridFSAdapter);
@@ -0,0 +1,9 @@
1
+ import express from 'express';
2
+ import { type GridFSFileUploadsModule } from './module.ts';
3
+ import { type Context } from '@unchainedshop/api';
4
+ declare const gridfsHandler: (req: express.Request & {
5
+ unchainedContext: Context & {
6
+ modules: GridFSFileUploadsModule;
7
+ };
8
+ }, res: express.Response) => Promise<void>;
9
+ export default gridfsHandler;
@@ -0,0 +1,110 @@
1
+ import { pipeline, finished } from 'node:stream/promises';
2
+ import { PassThrough } from 'node:stream';
3
+ import { buildHashedFilename } from '@unchainedshop/file-upload';
4
+ import express from 'express';
5
+ import sign from "./sign.js";
6
+ import {} from "./module.js";
7
+ import {} from '@unchainedshop/api';
8
+ import { createLogger } from '@unchainedshop/logger';
9
+ import { getFileAdapter } from '@unchainedshop/core-files';
10
+ import { timingSafeStringEqual } from '@unchainedshop/utils';
11
+ const { ROOT_URL, GRIDFS_PUT_SERVER_PATH = '/gridfs' } = process.env;
12
+ const logger = createLogger('unchained:gridfs');
13
+ const gridfsHandler = async (req, res) => {
14
+ try {
15
+ const { services, modules } = req.unchainedContext;
16
+ const url = new URL(req.url, ROOT_URL);
17
+ const [, directoryNameRaw, fileNameRaw] = url.pathname.split('/');
18
+ const directoryName = decodeURIComponent(directoryNameRaw);
19
+ const fileName = decodeURIComponent(fileNameRaw);
20
+ res.setHeader('Access-Control-Allow-Methods', 'GET, PUT');
21
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
22
+ res.setHeader('Access-Control-Allow-Origin', '*');
23
+ if (req.method === 'OPTIONS') {
24
+ res.status(200).end();
25
+ return;
26
+ }
27
+ if (req.method === 'PUT') {
28
+ const { s: signature, e: expiryTimestamp } = req.query;
29
+ const expiryDate = new Date(parseInt(expiryTimestamp, 10));
30
+ const fileId = await buildHashedFilename(directoryName, fileName, expiryDate);
31
+ const expectedSignature = await sign(directoryName, fileId, expiryDate.getTime());
32
+ if (await timingSafeStringEqual(expectedSignature, signature)) {
33
+ const file = await modules.files.findFile({ fileId });
34
+ if (!file) {
35
+ res.status(404).send('File not found');
36
+ return;
37
+ }
38
+ if (file.expires === null) {
39
+ res.status(400).send('File already linked');
40
+ return;
41
+ }
42
+ const type = req.header('Content-Type') === 'application/octet-stream'
43
+ ? file.type || req.header('Content-Type')
44
+ : req.header('Content-Type') || file.type;
45
+ const writeStream = await modules.gridfsFileUploads.createWriteStream(directoryName, fileId, fileName, {
46
+ chunkSizeBytes: 1024 * 1024,
47
+ metadata: { 'content-type': type },
48
+ });
49
+ await pipeline(req, new PassThrough({ highWaterMark: 1024 * 1024 * 4 }), writeStream);
50
+ await finished(writeStream, { readable: false });
51
+ const { length } = writeStream;
52
+ await services.files.linkFile({ fileId, size: length, type });
53
+ res.status(200).end();
54
+ return;
55
+ }
56
+ res.status(403).end();
57
+ return;
58
+ }
59
+ if (req.method === 'GET') {
60
+ const { s: signature, e: expiryTimestamp } = req.query;
61
+ const fileDocument = await modules.files.findFile({
62
+ url: `${GRIDFS_PUT_SERVER_PATH}/${directoryName}/${fileName}`,
63
+ });
64
+ if (fileDocument?.meta?.isPrivate) {
65
+ const expiry = parseInt(expiryTimestamp, 10);
66
+ if (expiry <= Date.now()) {
67
+ res.status(403).send('Access restricted: Expired.');
68
+ return;
69
+ }
70
+ const fileUploadAdapter = getFileAdapter();
71
+ const signedUrl = await fileUploadAdapter.createDownloadURL(fileDocument, expiry);
72
+ const expectedSignature = signedUrl ? new URL(signedUrl, 'file://').searchParams.get('s') : null;
73
+ if (!signedUrl ||
74
+ !expectedSignature ||
75
+ !(await timingSafeStringEqual(expectedSignature, signature))) {
76
+ res.status(403).send('Access restricted: Invalid signature.');
77
+ return;
78
+ }
79
+ }
80
+ else if (!fileDocument) {
81
+ res.status(404).end();
82
+ return;
83
+ }
84
+ const file = await modules.gridfsFileUploads.getFileInfo(directoryName, fileDocument._id);
85
+ if (file?.metadata?.['content-type']) {
86
+ res.setHeader('Content-Type', file.metadata['content-type']);
87
+ }
88
+ if (file?.length) {
89
+ res.setHeader('Content-Length', file?.length.toString());
90
+ }
91
+ const readStream = await modules.gridfsFileUploads.createReadStream(directoryName, fileDocument._id);
92
+ readStream.pipe(res, { end: false });
93
+ await finished(readStream);
94
+ res.status(200).end();
95
+ return;
96
+ }
97
+ res.status(404).end();
98
+ }
99
+ catch (e) {
100
+ if (e.code === 'ENOENT') {
101
+ logger.warn(e);
102
+ res.status(404).send();
103
+ }
104
+ else {
105
+ logger.warn(e);
106
+ res.status(503).send({ name: e.name, code: e.code, message: e.message });
107
+ }
108
+ }
109
+ };
110
+ export default gridfsHandler;
@@ -0,0 +1,3 @@
1
+ import { type RouteHandlerMethod } from 'fastify';
2
+ declare const gridfsHandler: RouteHandlerMethod;
3
+ export default gridfsHandler;
@@ -0,0 +1,138 @@
1
+ import { finished, pipeline } from 'node:stream/promises';
2
+ import { PassThrough } from 'node:stream';
3
+ import {} from 'fastify';
4
+ import { buildHashedFilename } from '@unchainedshop/file-upload';
5
+ import sign from "./sign.js";
6
+ import {} from "./module.js";
7
+ import {} from '@unchainedshop/api';
8
+ import { createLogger } from '@unchainedshop/logger';
9
+ import { getFileAdapter } from '@unchainedshop/core-files';
10
+ import { timingSafeStringEqual } from '@unchainedshop/utils';
11
+ const logger = createLogger('unchained:gridfs');
12
+ const { GRIDFS_PUT_SERVER_PATH = '/gridfs' } = process.env;
13
+ const gridfsHandler = async (req, reply) => {
14
+ try {
15
+ const { services, modules } = req.unchainedContext;
16
+ const { directoryName, fileName } = req.params;
17
+ reply.header('Access-Control-Allow-Methods', 'GET, PUT');
18
+ reply.header('Access-Control-Allow-Headers', 'content-type');
19
+ reply.header('Access-Control-Allow-Origin', '*');
20
+ if (req.method === 'OPTIONS') {
21
+ reply.status(200);
22
+ return reply.send();
23
+ }
24
+ if (req.method === 'PUT') {
25
+ const { s: signature, e: expiryTimestamp } = req.query;
26
+ const expiryDate = new Date(parseInt(expiryTimestamp, 10));
27
+ const fileId = await buildHashedFilename(directoryName, fileName, expiryDate);
28
+ const expectedSignature = await sign(directoryName, fileId, expiryDate.getTime());
29
+ if (await timingSafeStringEqual(expectedSignature, signature)) {
30
+ const file = await modules.files.findFile({ fileId });
31
+ if (!file) {
32
+ reply.status(404);
33
+ logger.error('File not found', { fileId });
34
+ return reply.send({
35
+ success: false,
36
+ message: 'File not found',
37
+ name: 'FILE_NOT_FOUND',
38
+ });
39
+ }
40
+ if (file.expires === null) {
41
+ reply.status(400);
42
+ logger.error('File already linked', { fileId });
43
+ return reply.send({
44
+ success: false,
45
+ message: 'File already linked',
46
+ name: 'FILE_ALREADY_LINKED',
47
+ });
48
+ }
49
+ const type = req.headers['content-type'] === 'application/octet-stream'
50
+ ? file.type || req.headers['content-type']
51
+ : req.headers['content-type'] || file.type;
52
+ const writeStream = await modules.gridfsFileUploads.createWriteStream(directoryName, fileId, fileName, {
53
+ chunkSizeBytes: 1024 * 1024,
54
+ metadata: { 'content-type': type },
55
+ });
56
+ await pipeline(req.raw, new PassThrough({ highWaterMark: 1024 * 1024 * 4 }), writeStream);
57
+ await finished(writeStream, { readable: false });
58
+ const { length } = writeStream;
59
+ await services.files.linkFile({ fileId, size: length, type });
60
+ reply.status(200);
61
+ return reply.send({
62
+ success: true,
63
+ fileId,
64
+ size: length,
65
+ type,
66
+ });
67
+ }
68
+ logger.error('Invalid signature', { fileId, expiryDate });
69
+ reply.status(403);
70
+ return reply.send({
71
+ success: false,
72
+ message: 'Access restricted: Invalid signature.',
73
+ name: 'INVALID_SIGNATURE',
74
+ });
75
+ }
76
+ if (req.method === 'GET') {
77
+ const { s: signature, e: expiryTimestamp } = req.query;
78
+ const fileDocument = await modules.files.findFile({
79
+ url: `${GRIDFS_PUT_SERVER_PATH}/${directoryName}/${fileName}`,
80
+ });
81
+ if (fileDocument?.meta?.isPrivate) {
82
+ const expiry = parseInt(expiryTimestamp, 10);
83
+ if (expiry <= Date.now()) {
84
+ logger.error('URL Expired', { fileName, expiry });
85
+ reply.status(403);
86
+ return reply.send({
87
+ success: false,
88
+ message: 'Access restricted: URL expired.',
89
+ name: 'URL_EXPIRED',
90
+ });
91
+ }
92
+ const fileUploadAdapter = getFileAdapter();
93
+ const signedUrl = await fileUploadAdapter.createDownloadURL(fileDocument, expiry);
94
+ const expectedSignature = signedUrl ? new URL(signedUrl, 'file://').searchParams.get('s') : null;
95
+ if (!signedUrl ||
96
+ !expectedSignature ||
97
+ !(await timingSafeStringEqual(expectedSignature, signature))) {
98
+ reply.status(403);
99
+ logger.error('Invalid signature', { fileName, expiry });
100
+ return reply.send({
101
+ success: false,
102
+ message: 'Access restricted: Invalid signature.',
103
+ name: 'INVALID_SIGNATURE',
104
+ });
105
+ }
106
+ }
107
+ else if (!fileDocument) {
108
+ logger.error('File not found', { fileName });
109
+ reply.status(404);
110
+ return reply.send();
111
+ }
112
+ const file = await modules.gridfsFileUploads.getFileInfo(directoryName, fileDocument._id);
113
+ if (file?.metadata?.['content-type']) {
114
+ reply.header('content-type', file.metadata['content-type']);
115
+ }
116
+ if (file?.length) {
117
+ reply.header('content-length', file?.length.toString());
118
+ }
119
+ const readStream = await modules.gridfsFileUploads.createReadStream(directoryName, fileDocument._id);
120
+ reply.status(200);
121
+ return reply.send(readStream);
122
+ }
123
+ reply.status(404);
124
+ return reply.send();
125
+ }
126
+ catch (e) {
127
+ logger.error(e);
128
+ if (e.code === 'ENOENT') {
129
+ reply.status(404);
130
+ return reply.send();
131
+ }
132
+ else {
133
+ reply.status(504);
134
+ return reply.send({ success: false, name: e.name, code: e.code, message: e.message });
135
+ }
136
+ }
137
+ };
138
+ export default gridfsHandler;
@@ -1,5 +1,4 @@
1
- import type { IPlugin } from '@unchainedshop/core';
2
- export declare const GridFSPlugin: IPlugin;
3
- export default GridFSPlugin;
4
- export { GridFSAdapter } from './adapter.ts';
5
- export { type GridFSFileUploadsModule } from './module.ts';
1
+ import './adapter.ts';
2
+ import gridfsModules, { type GridFSFileUploadsModule } from './module.ts';
3
+ export type { GridFSFileUploadsModule };
4
+ export default gridfsModules;
@@ -1,30 +1,3 @@
1
- import { createLogger } from '@unchainedshop/logger';
2
- import { GridFSAdapter } from "./adapter.js";
3
- import { configureGridFSFileUploadModule } from "./module.js";
4
- import { gridfsRouteHandler } from "./api.js";
5
- const { GRIDFS_PUT_SERVER_PATH = '/gridfs', UNCHAINED_GRIDFS_PUT_UPLOAD_SECRET } = process.env;
6
- const logger = createLogger('unchained:gridfs');
7
- export const GridFSPlugin = {
8
- key: 'shop.unchained.file-upload-plugin.gridfs',
9
- label: 'GridFS File Upload Plugin',
10
- version: '1.0.0',
11
- adapters: [GridFSAdapter],
12
- module: ({ db }) => ({
13
- gridfsFileUploads: configureGridFSFileUploadModule({ db }),
14
- }),
15
- routes: [
16
- {
17
- path: `${GRIDFS_PUT_SERVER_PATH}/:directoryName/:fileName`,
18
- method: 'ALL',
19
- handler: gridfsRouteHandler,
20
- },
21
- ],
22
- onRegister: () => {
23
- if (!UNCHAINED_GRIDFS_PUT_UPLOAD_SECRET) {
24
- logger.warn('UNCHAINED_GRIDFS_PUT_UPLOAD_SECRET not set - PUT uploads and signed downloads will fail');
25
- }
26
- },
27
- };
28
- export default GridFSPlugin;
29
- export { GridFSAdapter } from "./adapter.js";
30
- export {} from "./module.js";
1
+ import "./adapter.js";
2
+ import gridfsModules, {} from "./module.js";
3
+ export default gridfsModules;
@@ -0,0 +1,2 @@
1
+ declare const minioHandler: (req: any, res: any) => Promise<void>;
2
+ export default minioHandler;
@@ -0,0 +1,36 @@
1
+ import { createLogger } from '@unchainedshop/logger';
2
+ import { timingSafeStringEqual } from '@unchainedshop/utils';
3
+ const logger = createLogger('unchained:minio');
4
+ const { MINIO_WEBHOOK_AUTH_TOKEN } = process.env;
5
+ const isAuthorized = async ({ authorization = '' }) => {
6
+ if (!MINIO_WEBHOOK_AUTH_TOKEN)
7
+ return false;
8
+ const [type, token] = authorization.split(' ');
9
+ if (type !== 'Bearer' || !token)
10
+ return false;
11
+ return timingSafeStringEqual(token, MINIO_WEBHOOK_AUTH_TOKEN);
12
+ };
13
+ const minioHandler = async (req, res) => {
14
+ try {
15
+ if (req.method === 'POST' && req.body) {
16
+ const { headers } = req;
17
+ const { Records = [], EventName } = req.body;
18
+ if (EventName === 's3:ObjectCreated:Put' && (await isAuthorized(headers))) {
19
+ const [{ s3 }] = Records;
20
+ const { object } = s3;
21
+ const { size, contentType: type } = object;
22
+ const [fileId] = object.key.split('.');
23
+ const { services } = req.unchainedContext;
24
+ await services.files.linkFile({ fileId, type, size });
25
+ res.status(200).end();
26
+ return;
27
+ }
28
+ }
29
+ res.status(404).end();
30
+ }
31
+ catch (e) {
32
+ logger.error(e);
33
+ res.status(503).send({ name: e.name, code: e.code, message: e.message });
34
+ }
35
+ };
36
+ export default minioHandler;
@@ -0,0 +1,3 @@
1
+ import { type RouteHandlerMethod } from 'fastify';
2
+ declare const minioHandler: RouteHandlerMethod;
3
+ export default minioHandler;
@@ -0,0 +1,40 @@
1
+ import {} from '@unchainedshop/api';
2
+ import { createLogger } from '@unchainedshop/logger';
3
+ import {} from 'fastify';
4
+ import { timingSafeStringEqual } from '@unchainedshop/utils';
5
+ const logger = createLogger('unchained:minio');
6
+ const { MINIO_WEBHOOK_AUTH_TOKEN } = process.env;
7
+ const isAuthorized = async ({ authorization = '' }) => {
8
+ if (!MINIO_WEBHOOK_AUTH_TOKEN)
9
+ return false;
10
+ const [type, token] = authorization.split(' ');
11
+ if (type !== 'Bearer' || !token)
12
+ return false;
13
+ return timingSafeStringEqual(token, MINIO_WEBHOOK_AUTH_TOKEN);
14
+ };
15
+ const minioHandler = async (req, res) => {
16
+ try {
17
+ if (req.body) {
18
+ const { headers } = req;
19
+ const { Records = [], EventName } = req.body;
20
+ if (EventName === 's3:ObjectCreated:Put' && (await isAuthorized(headers))) {
21
+ const [{ s3 }] = Records;
22
+ const { object } = s3;
23
+ const { size, contentType: type } = object;
24
+ const [fileId] = object.key.split('.');
25
+ const { services } = req.unchainedContext;
26
+ await services.files.linkFile({ fileId, type, size });
27
+ res.status(200);
28
+ return res.send();
29
+ }
30
+ }
31
+ res.status(404);
32
+ return res.send();
33
+ }
34
+ catch (e) {
35
+ logger.error(e);
36
+ res.status(503);
37
+ return res.send({ name: e.name, code: e.code, message: e.message });
38
+ }
39
+ };
40
+ export default minioHandler;
@@ -1,4 +1,5 @@
1
- import type { IPlugin } from '@unchainedshop/core';
2
- export declare const MinioPlugin: IPlugin;
3
- export default MinioPlugin;
4
- export { MinioAdapter, connectToMinio } from './adapter.ts';
1
+ import { type IFileAdapter } from '@unchainedshop/file-upload';
2
+ import { Client } from 'minio';
3
+ export declare function connectToMinio(): Promise<Client | null>;
4
+ export declare const MinioAdapter: IFileAdapter;
5
+ export default MinioAdapter;