@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.
- package/README.md +2 -0
- package/lib/delivery/post.d.ts +1 -0
- package/lib/delivery/post.js +23 -0
- package/lib/delivery/send-message.d.ts +1 -0
- package/lib/delivery/send-message.js +40 -0
- package/lib/delivery/stores.d.ts +1 -0
- package/lib/delivery/stores.js +43 -0
- package/lib/enrollments/licensed.d.ts +4 -0
- package/lib/enrollments/licensed.js +52 -0
- package/lib/events/node-event-emitter.d.ts +1 -3
- package/lib/events/node-event-emitter.js +3 -3
- package/lib/files/gridfs/adapter.d.ts +2 -2
- package/lib/files/gridfs/adapter.js +5 -2
- package/lib/files/gridfs/handler-express.d.ts +9 -0
- package/lib/files/gridfs/handler-express.js +110 -0
- package/lib/files/gridfs/handler-fastify.d.ts +3 -0
- package/lib/files/gridfs/handler-fastify.js +138 -0
- package/lib/files/gridfs/index.d.ts +4 -5
- package/lib/files/gridfs/index.js +3 -30
- package/lib/files/minio/handler-express.d.ts +2 -0
- package/lib/files/minio/handler-express.js +36 -0
- package/lib/files/minio/handler-fastify.d.ts +3 -0
- package/lib/files/minio/handler-fastify.js +40 -0
- package/lib/files/minio/index.d.ts +5 -4
- package/lib/files/minio/index.js +157 -18
- package/lib/filters/local-search.d.ts +3 -0
- package/lib/filters/local-search.js +66 -0
- package/lib/filters/strict-equal.d.ts +1 -0
- package/lib/filters/strict-equal.js +24 -0
- package/lib/payment/apple-iap/adapter.d.ts +1 -2
- package/lib/payment/apple-iap/adapter.js +3 -2
- package/lib/payment/apple-iap/handler-express.d.ts +2 -0
- package/lib/payment/apple-iap/handler-express.js +103 -0
- package/lib/payment/apple-iap/handler-fastify.d.ts +3 -0
- package/lib/payment/apple-iap/handler-fastify.js +102 -0
- package/lib/payment/apple-iap/index.d.ts +4 -6
- package/lib/payment/apple-iap/index.js +3 -28
- package/lib/payment/braintree.d.ts +1 -0
- package/lib/payment/braintree.js +138 -0
- package/lib/payment/cryptopay/handler-express.d.ts +2 -0
- package/lib/payment/cryptopay/handler-express.js +19 -0
- package/lib/payment/cryptopay/handler-fastify.d.ts +3 -0
- package/lib/payment/cryptopay/handler-fastify.js +17 -0
- package/lib/payment/cryptopay/index.d.ts +4 -5
- package/lib/payment/cryptopay/index.js +3 -31
- package/lib/payment/cryptopay/plugin.d.ts +3 -0
- package/lib/payment/cryptopay/plugin.js +159 -0
- package/lib/payment/datatrans-v2/handler-express.d.ts +1 -0
- package/lib/payment/datatrans-v2/handler-express.js +71 -0
- package/lib/payment/datatrans-v2/handler-fastify.d.ts +2 -0
- package/lib/payment/datatrans-v2/handler-fastify.js +71 -0
- package/lib/payment/datatrans-v2/index.d.ts +1 -4
- package/lib/payment/datatrans-v2/index.js +400 -17
- package/lib/payment/invoice-prepaid.d.ts +1 -0
- package/lib/payment/invoice-prepaid.js +30 -0
- package/lib/payment/invoice.d.ts +1 -0
- package/lib/payment/invoice.js +27 -0
- package/lib/payment/paypal-checkout.d.ts +1 -0
- package/lib/payment/paypal-checkout.js +87 -0
- package/lib/payment/payrexx/handler-express.d.ts +1 -0
- package/lib/payment/payrexx/handler-express.js +85 -0
- package/lib/payment/payrexx/handler-fastify.d.ts +2 -0
- package/lib/payment/payrexx/handler-fastify.js +91 -0
- package/lib/payment/payrexx/index.d.ts +1 -4
- package/lib/payment/payrexx/index.js +158 -19
- package/lib/payment/postfinance-checkout/handler-express.d.ts +1 -0
- package/lib/payment/postfinance-checkout/handler-express.js +37 -0
- package/lib/payment/postfinance-checkout/handler-fastify.d.ts +2 -0
- package/lib/payment/postfinance-checkout/handler-fastify.js +40 -0
- package/lib/payment/postfinance-checkout/index.d.ts +0 -5
- package/lib/payment/postfinance-checkout/index.js +186 -25
- package/lib/payment/saferpay/adapter.js +2 -1
- package/lib/payment/saferpay/handler-express.d.ts +1 -0
- package/lib/payment/saferpay/handler-express.js +59 -0
- package/lib/payment/saferpay/handler-fastify.d.ts +2 -0
- package/lib/payment/saferpay/handler-fastify.js +62 -0
- package/lib/payment/saferpay/index.d.ts +4 -5
- package/lib/payment/saferpay/index.js +3 -29
- package/lib/payment/stripe/handler-express.d.ts +5 -0
- package/lib/payment/stripe/handler-express.js +121 -0
- package/lib/payment/stripe/handler-fastify.d.ts +6 -0
- package/lib/payment/stripe/handler-fastify.js +131 -0
- package/lib/payment/stripe/index.d.ts +1 -4
- package/lib/payment/stripe/index.js +116 -22
- package/lib/payment/stripe/stripe.js +4 -1
- package/lib/presets/all-express.d.ts +6 -0
- package/lib/presets/all-express.js +26 -0
- package/lib/presets/all-fastify.d.ts +6 -0
- package/lib/presets/all-fastify.js +49 -0
- package/lib/presets/all.d.ts +70 -1
- package/lib/presets/all.js +27 -41
- package/lib/presets/base-express.d.ts +2 -0
- package/lib/presets/base-express.js +5 -0
- package/lib/presets/base-fastify.d.ts +3 -0
- package/lib/presets/base-fastify.js +16 -0
- package/lib/presets/base.d.ts +25 -1
- package/lib/presets/base.js +25 -56
- package/lib/presets/countries/ch.d.ts +2 -1
- package/lib/presets/countries/ch.js +2 -7
- package/lib/presets/crypto-express.d.ts +3 -0
- package/lib/presets/crypto-express.js +8 -0
- package/lib/presets/crypto-fastify.d.ts +3 -0
- package/lib/presets/crypto-fastify.js +11 -0
- package/lib/presets/crypto.d.ts +7 -3
- package/lib/presets/crypto.js +7 -18
- package/lib/pricing/delivery-swiss-tax.d.ts +2 -0
- package/lib/pricing/delivery-swiss-tax.js +62 -0
- package/lib/pricing/discount-100-off.d.ts +2 -0
- package/lib/pricing/discount-100-off.js +30 -0
- package/lib/pricing/discount-half-price-manual.d.ts +2 -0
- package/lib/pricing/discount-half-price-manual.js +30 -0
- package/lib/pricing/discount-half-price.d.ts +2 -0
- package/lib/pricing/discount-half-price.js +33 -0
- package/lib/pricing/free-delivery.d.ts +2 -0
- package/lib/pricing/free-delivery.js +27 -0
- package/lib/pricing/free-payment.d.ts +2 -0
- package/lib/pricing/free-payment.js +27 -0
- package/lib/pricing/order-delivery.d.ts +2 -0
- package/lib/pricing/order-delivery.js +33 -0
- package/lib/pricing/order-discount.d.ts +2 -0
- package/lib/pricing/order-discount.js +74 -0
- package/lib/pricing/order-items-discount.d.ts +2 -0
- package/lib/pricing/order-items-discount.js +50 -0
- package/lib/pricing/order-items.d.ts +2 -0
- package/lib/pricing/order-items.js +43 -0
- package/lib/pricing/order-payment.d.ts +2 -0
- package/lib/pricing/order-payment.js +33 -0
- package/lib/pricing/order-round.d.ts +10 -0
- package/lib/pricing/order-round.js +135 -0
- package/lib/pricing/product-catalog-price-options.d.ts +2 -0
- package/lib/pricing/product-catalog-price-options.js +51 -0
- package/lib/pricing/product-catalog-price.d.ts +2 -0
- package/lib/pricing/product-catalog-price.js +60 -0
- package/lib/pricing/product-discount.d.ts +2 -0
- package/lib/pricing/product-discount.js +69 -0
- package/lib/pricing/product-price-rateconversion.d.ts +2 -0
- package/lib/pricing/product-price-rateconversion.js +53 -0
- package/lib/pricing/product-round.d.ts +10 -0
- package/lib/pricing/product-round.js +44 -0
- package/lib/pricing/product-swiss-tax.d.ts +2 -0
- package/lib/pricing/product-swiss-tax.js +76 -0
- package/lib/quotations/manual.d.ts +1 -0
- package/lib/quotations/manual.js +20 -0
- package/lib/warehousing/eth-minter.d.ts +1 -0
- package/lib/warehousing/eth-minter.js +120 -0
- package/lib/warehousing/store.d.ts +1 -0
- package/lib/warehousing/store.js +34 -0
- package/lib/worker/budgetsms.d.ts +9 -0
- package/lib/worker/budgetsms.js +169 -0
- package/lib/worker/bulk-export.d.ts +2 -0
- package/lib/worker/bulk-export.js +64 -0
- package/lib/worker/bulk-import.d.ts +2 -0
- package/lib/worker/bulk-import.js +65 -0
- package/lib/worker/bulkgate.d.ts +1 -0
- package/lib/worker/bulkgate.js +91 -0
- package/lib/worker/email.d.ts +1 -0
- package/lib/worker/email.js +136 -0
- package/lib/worker/enrollment-order-generator.d.ts +1 -0
- package/lib/worker/enrollment-order-generator.js +77 -0
- package/lib/worker/error-notifications.d.ts +9 -0
- package/lib/worker/error-notifications.js +61 -0
- package/lib/worker/export-token.d.ts +4 -0
- package/lib/worker/export-token.js +26 -0
- package/lib/worker/external.d.ts +2 -0
- package/lib/worker/external.js +13 -0
- package/lib/worker/heartbeat.d.ts +1 -0
- package/lib/worker/heartbeat.js +26 -0
- package/lib/worker/http-request.d.ts +8 -0
- package/lib/worker/http-request.js +56 -0
- package/lib/worker/message.d.ts +10 -0
- package/lib/worker/message.js +42 -0
- package/lib/worker/push-notification.d.ts +1 -0
- package/lib/worker/push-notification.js +87 -0
- package/lib/worker/twilio.d.ts +1 -0
- package/lib/worker/twilio.js +59 -0
- package/lib/worker/update-coinbase-rates.d.ts +1 -0
- package/lib/worker/update-coinbase-rates.js +59 -0
- package/lib/worker/update-ecb-rates.d.ts +1 -0
- package/lib/worker/update-ecb-rates.js +92 -0
- package/lib/worker/update-token-ownership.d.ts +3 -0
- package/lib/worker/update-token-ownership.js +57 -0
- package/lib/worker/zombie-killer.d.ts +13 -0
- package/lib/worker/zombie-killer.js +91 -0
- 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,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,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import {} from '@unchainedshop/events';
|
|
3
|
-
|
|
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
|
-
|
|
14
|
+
setEmitAdapter(NodeEventEmitter());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type IFileAdapter } from '@unchainedshop/
|
|
1
|
+
import { type IFileAdapter } from '@unchainedshop/file-upload';
|
|
2
2
|
import { type UnchainedCore } from '@unchainedshop/core';
|
|
3
|
-
import { type GridFSFileUploadsModule } from './
|
|
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,
|
|
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 "./
|
|
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,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
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
export
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
|
|
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,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,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
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
export
|
|
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;
|