@unchainedshop/plugins 4.6.0 → 4.6.2
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/lib/delivery/post/adapter.d.ts +2 -0
- package/lib/delivery/post/adapter.js +22 -0
- package/lib/delivery/post/index.d.ts +4 -0
- package/lib/delivery/post/index.js +10 -0
- package/lib/delivery/send-message/adapter.d.ts +2 -0
- package/lib/delivery/send-message/adapter.js +39 -0
- package/lib/delivery/send-message/index.d.ts +4 -0
- package/lib/delivery/send-message/index.js +10 -0
- package/lib/delivery/stores/adapter.d.ts +2 -0
- package/lib/delivery/stores/adapter.js +42 -0
- package/lib/delivery/stores/index.d.ts +4 -0
- package/lib/delivery/stores/index.js +10 -0
- package/lib/enrollments/licensed/adapter.d.ts +6 -0
- package/lib/enrollments/licensed/adapter.js +51 -0
- package/lib/enrollments/licensed/index.d.ts +4 -0
- package/lib/enrollments/licensed/index.js +10 -0
- package/lib/files/gridfs/api.d.ts +7 -0
- package/lib/files/gridfs/api.js +163 -0
- package/lib/files/minio/adapter.d.ts +4 -0
- package/lib/files/minio/adapter.js +160 -0
- package/lib/files/minio/api.d.ts +2 -0
- package/lib/files/minio/api.js +41 -0
- package/lib/files/temp-upload/api.d.ts +6 -0
- package/lib/files/temp-upload/api.js +49 -0
- package/lib/files/temp-upload/index.d.ts +3 -0
- package/lib/files/temp-upload/index.js +15 -0
- package/lib/filters/local-search/adapter.d.ts +2 -0
- package/lib/filters/local-search/adapter.js +56 -0
- package/lib/filters/local-search/index.d.ts +4 -0
- package/lib/filters/local-search/index.js +16 -0
- package/lib/filters/strict-equal/adapter.d.ts +2 -0
- package/lib/filters/strict-equal/adapter.js +23 -0
- package/lib/filters/strict-equal/index.d.ts +4 -0
- package/lib/filters/strict-equal/index.js +10 -0
- package/lib/payment/apple-iap/api.d.ts +2 -0
- package/lib/payment/apple-iap/api.js +119 -0
- package/lib/payment/cryptopay/adapter.d.ts +2 -0
- package/lib/payment/cryptopay/adapter.js +157 -0
- package/lib/payment/cryptopay/api.d.ts +2 -0
- package/lib/payment/cryptopay/api.js +23 -0
- package/lib/payment/datatrans-v2/adapter.d.ts +2 -0
- package/lib/payment/datatrans-v2/adapter.js +406 -0
- package/lib/payment/datatrans-v2/api.d.ts +2 -0
- package/lib/payment/datatrans-v2/api.js +108 -0
- package/lib/payment/invoice/adapter.d.ts +2 -0
- package/lib/payment/invoice/adapter.js +26 -0
- package/lib/payment/invoice/index.d.ts +4 -0
- package/lib/payment/invoice/index.js +10 -0
- package/lib/payment/invoice-prepaid/adapter.d.ts +2 -0
- package/lib/payment/invoice-prepaid/adapter.js +29 -0
- package/lib/payment/invoice-prepaid/index.d.ts +4 -0
- package/lib/payment/invoice-prepaid/index.js +10 -0
- package/lib/payment/payrexx/adapter.d.ts +2 -0
- package/lib/payment/payrexx/adapter.js +161 -0
- package/lib/payment/payrexx/api.d.ts +2 -0
- package/lib/payment/payrexx/api.js +97 -0
- package/lib/payment/postfinance-checkout/adapter.d.ts +2 -0
- package/lib/payment/postfinance-checkout/adapter.js +189 -0
- package/lib/payment/postfinance-checkout/webhook.d.ts +2 -0
- package/lib/payment/postfinance-checkout/webhook.js +48 -0
- package/lib/payment/saferpay/webhook.d.ts +2 -0
- package/lib/payment/saferpay/webhook.js +64 -0
- package/lib/payment/stripe/adapter.d.ts +2 -0
- package/lib/payment/stripe/adapter.js +121 -0
- package/lib/payment/stripe/api.d.ts +6 -0
- package/lib/payment/stripe/api.js +165 -0
- package/lib/payment/stripe/stripe.js +12 -10
- package/lib/presets/base.d.ts +1 -0
- package/lib/presets/base.js +1 -0
- package/lib/pricing/delivery-swiss-tax/adapter.d.ts +3 -0
- package/lib/pricing/delivery-swiss-tax/adapter.js +62 -0
- package/lib/pricing/delivery-swiss-tax/index.d.ts +4 -0
- package/lib/pricing/delivery-swiss-tax/index.js +10 -0
- package/lib/pricing/discount-100-off/adapter.d.ts +3 -0
- package/lib/pricing/discount-100-off/adapter.js +30 -0
- package/lib/pricing/discount-100-off/index.d.ts +4 -0
- package/lib/pricing/discount-100-off/index.js +10 -0
- package/lib/pricing/discount-half-price-manual/adapter.d.ts +3 -0
- package/lib/pricing/discount-half-price-manual/adapter.js +30 -0
- package/lib/pricing/discount-half-price-manual/index.d.ts +4 -0
- package/lib/pricing/discount-half-price-manual/index.js +10 -0
- package/lib/pricing/free-delivery/adapter.d.ts +3 -0
- package/lib/pricing/free-delivery/adapter.js +27 -0
- package/lib/pricing/free-delivery/index.d.ts +4 -0
- package/lib/pricing/free-delivery/index.js +10 -0
- package/lib/pricing/free-payment/adapter.d.ts +3 -0
- package/lib/pricing/free-payment/adapter.js +27 -0
- package/lib/pricing/free-payment/index.d.ts +4 -0
- package/lib/pricing/free-payment/index.js +10 -0
- package/lib/pricing/order-delivery/adapter.d.ts +3 -0
- package/lib/pricing/order-delivery/adapter.js +33 -0
- package/lib/pricing/order-delivery/index.d.ts +4 -0
- package/lib/pricing/order-delivery/index.js +10 -0
- package/lib/pricing/order-discount/adapter.d.ts +3 -0
- package/lib/pricing/order-discount/adapter.js +74 -0
- package/lib/pricing/order-discount/index.d.ts +4 -0
- package/lib/pricing/order-discount/index.js +10 -0
- package/lib/pricing/order-items/adapter.d.ts +3 -0
- package/lib/pricing/order-items/adapter.js +43 -0
- package/lib/pricing/order-items/index.d.ts +4 -0
- package/lib/pricing/order-items/index.js +10 -0
- package/lib/pricing/order-payment/adapter.d.ts +3 -0
- package/lib/pricing/order-payment/adapter.js +33 -0
- package/lib/pricing/order-payment/index.d.ts +4 -0
- package/lib/pricing/order-payment/index.js +10 -0
- package/lib/pricing/product-catalog-price/adapter.d.ts +3 -0
- package/lib/pricing/product-catalog-price/adapter.js +60 -0
- package/lib/pricing/product-catalog-price/index.d.ts +4 -0
- package/lib/pricing/product-catalog-price/index.js +10 -0
- package/lib/pricing/product-discount/adapter.d.ts +3 -0
- package/lib/pricing/product-discount/adapter.js +69 -0
- package/lib/pricing/product-discount/index.d.ts +4 -0
- package/lib/pricing/product-discount/index.js +10 -0
- package/lib/pricing/product-price-rateconversion/adapter.d.ts +3 -0
- package/lib/pricing/product-price-rateconversion/adapter.js +53 -0
- package/lib/pricing/product-price-rateconversion/index.d.ts +4 -0
- package/lib/pricing/product-price-rateconversion/index.js +10 -0
- package/lib/pricing/product-swiss-tax/adapter.d.ts +3 -0
- package/lib/pricing/product-swiss-tax/adapter.js +76 -0
- package/lib/pricing/product-swiss-tax/index.d.ts +4 -0
- package/lib/pricing/product-swiss-tax/index.js +10 -0
- package/lib/quotations/manual/adapter.d.ts +2 -0
- package/lib/quotations/manual/adapter.js +19 -0
- package/lib/quotations/manual/index.d.ts +4 -0
- package/lib/quotations/manual/index.js +10 -0
- package/lib/warehousing/erc-metadata/api.d.ts +12 -0
- package/lib/warehousing/erc-metadata/api.js +43 -0
- package/lib/warehousing/erc-metadata/index.d.ts +3 -0
- package/lib/warehousing/erc-metadata/index.js +15 -0
- package/lib/warehousing/eth-minter/adapter.d.ts +2 -0
- package/lib/warehousing/eth-minter/adapter.js +119 -0
- package/lib/warehousing/eth-minter/index.d.ts +4 -0
- package/lib/warehousing/eth-minter/index.js +10 -0
- package/lib/warehousing/store/adapter.d.ts +2 -0
- package/lib/warehousing/store/adapter.js +33 -0
- package/lib/warehousing/store/index.d.ts +4 -0
- package/lib/warehousing/store/index.js +10 -0
- package/lib/worker/budgetsms/adapter.d.ts +22 -0
- package/lib/worker/budgetsms/adapter.js +169 -0
- package/lib/worker/budgetsms/index.d.ts +4 -0
- package/lib/worker/budgetsms/index.js +10 -0
- package/lib/worker/bulk-export/adapter.d.ts +3 -0
- package/lib/worker/bulk-export/adapter.js +64 -0
- package/lib/worker/bulk-export/index.d.ts +4 -0
- package/lib/worker/bulk-export/index.js +10 -0
- package/lib/worker/bulk-export.d.ts +2 -0
- package/lib/worker/bulk-export.js +64 -0
- package/lib/worker/bulk-import/adapter.d.ts +3 -0
- package/lib/worker/bulk-import/adapter.js +65 -0
- package/lib/worker/bulk-import/api.d.ts +7 -0
- package/lib/worker/bulk-import/api.js +53 -0
- package/lib/worker/bulk-import/index.d.ts +4 -0
- package/lib/worker/bulk-import/index.js +18 -0
- package/lib/worker/bulkgate/adapter.d.ts +12 -0
- package/lib/worker/bulkgate/adapter.js +91 -0
- package/lib/worker/bulkgate/index.d.ts +4 -0
- package/lib/worker/bulkgate/index.js +10 -0
- package/lib/worker/email/adapter.d.ts +9 -0
- package/lib/worker/email/adapter.js +150 -0
- package/lib/worker/email/index.d.ts +4 -0
- package/lib/worker/email/index.js +10 -0
- package/lib/worker/enrollment-order-generator/adapter.d.ts +4 -0
- package/lib/worker/enrollment-order-generator/adapter.js +77 -0
- package/lib/worker/enrollment-order-generator/index.d.ts +4 -0
- package/lib/worker/enrollment-order-generator/index.js +13 -0
- package/lib/worker/error-notifications/adapter.d.ts +10 -0
- package/lib/worker/error-notifications/adapter.js +62 -0
- package/lib/worker/error-notifications/index.d.ts +4 -0
- package/lib/worker/error-notifications/index.js +13 -0
- package/lib/worker/export-token/adapter.d.ts +4 -0
- package/lib/worker/export-token/adapter.js +25 -0
- package/lib/worker/export-token/index.d.ts +4 -0
- package/lib/worker/export-token/index.js +13 -0
- package/lib/worker/external/adapter.d.ts +3 -0
- package/lib/worker/external/adapter.js +13 -0
- package/lib/worker/external/index.d.ts +4 -0
- package/lib/worker/external/index.js +10 -0
- package/lib/worker/heartbeat/adapter.d.ts +8 -0
- package/lib/worker/heartbeat/adapter.js +26 -0
- package/lib/worker/heartbeat/index.d.ts +4 -0
- package/lib/worker/heartbeat/index.js +10 -0
- package/lib/worker/http-request/adapter.d.ts +8 -0
- package/lib/worker/http-request/adapter.js +55 -0
- package/lib/worker/http-request/index.d.ts +4 -0
- package/lib/worker/http-request/index.js +10 -0
- package/lib/worker/message/adapter.d.ts +11 -0
- package/lib/worker/message/adapter.js +42 -0
- package/lib/worker/message/index.d.ts +4 -0
- package/lib/worker/message/index.js +10 -0
- package/lib/worker/push-notification/adapter.d.ts +9 -0
- package/lib/worker/push-notification/adapter.js +87 -0
- package/lib/worker/push-notification/index.d.ts +4 -0
- package/lib/worker/push-notification/index.js +10 -0
- package/lib/worker/twilio/adapter.d.ts +8 -0
- package/lib/worker/twilio/adapter.js +59 -0
- package/lib/worker/twilio/index.d.ts +4 -0
- package/lib/worker/twilio/index.js +10 -0
- package/lib/worker/update-coinbase-rates/adapter.d.ts +4 -0
- package/lib/worker/update-coinbase-rates/adapter.js +61 -0
- package/lib/worker/update-coinbase-rates/index.d.ts +4 -0
- package/lib/worker/update-coinbase-rates/index.js +13 -0
- package/lib/worker/update-ecb-rates/adapter.d.ts +4 -0
- package/lib/worker/update-ecb-rates/adapter.js +94 -0
- package/lib/worker/update-ecb-rates/index.d.ts +4 -0
- package/lib/worker/update-ecb-rates/index.js +13 -0
- package/lib/worker/update-token-ownership/adapter.d.ts +5 -0
- package/lib/worker/update-token-ownership/adapter.js +58 -0
- package/lib/worker/update-token-ownership/index.d.ts +4 -0
- package/lib/worker/update-token-ownership/index.js +13 -0
- package/lib/worker/zombie-killer/adapter.d.ts +13 -0
- package/lib/worker/zombie-killer/adapter.js +90 -0
- package/lib/worker/zombie-killer/index.d.ts +4 -0
- package/lib/worker/zombie-killer/index.js +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DeliveryAdapter } from '@unchainedshop/core';
|
|
2
|
+
import { DeliveryProviderType } from '@unchainedshop/core-delivery';
|
|
3
|
+
export 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
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {} from '@unchainedshop/core';
|
|
2
|
+
import { Post } from "./adapter.js";
|
|
3
|
+
export const PostPlugin = {
|
|
4
|
+
key: 'shop.unchained.post',
|
|
5
|
+
label: 'Post Delivery Plugin',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
adapters: [Post],
|
|
8
|
+
};
|
|
9
|
+
export default PostPlugin;
|
|
10
|
+
export { Post } from "./adapter.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DeliveryAdapter } from '@unchainedshop/core';
|
|
2
|
+
import { DeliveryProviderType } from '@unchainedshop/core-delivery';
|
|
3
|
+
export 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
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {} from '@unchainedshop/core';
|
|
2
|
+
import { SendMessage } from "./adapter.js";
|
|
3
|
+
export const SendMessagePlugin = {
|
|
4
|
+
key: 'shop.unchained.delivery.send-message',
|
|
5
|
+
label: 'Send Message Delivery Plugin',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
adapters: [SendMessage],
|
|
8
|
+
};
|
|
9
|
+
export default SendMessagePlugin;
|
|
10
|
+
export { SendMessage } from "./adapter.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { DeliveryAdapter } from '@unchainedshop/core';
|
|
2
|
+
import { DeliveryProviderType } from '@unchainedshop/core-delivery';
|
|
3
|
+
export 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
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {} from '@unchainedshop/core';
|
|
2
|
+
import { PickMup } from "./adapter.js";
|
|
3
|
+
export const PickMupPlugin = {
|
|
4
|
+
key: 'shop.unchained.stores',
|
|
5
|
+
label: 'Pick-up Stores Delivery Plugin',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
adapters: [PickMup],
|
|
8
|
+
};
|
|
9
|
+
export default PickMupPlugin;
|
|
10
|
+
export { PickMup } from "./adapter.js";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { 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
|
+
export 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
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {} from '@unchainedshop/core';
|
|
2
|
+
import { LicensedEnrollments } from "./adapter.js";
|
|
3
|
+
export const LicensedEnrollmentsPlugin = {
|
|
4
|
+
key: 'shop.unchained.enrollments.licensed',
|
|
5
|
+
label: 'Licensed Enrollments Plugin',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
adapters: [LicensedEnrollments],
|
|
8
|
+
};
|
|
9
|
+
export default LicensedEnrollmentsPlugin;
|
|
10
|
+
export { LicensedEnrollments, rangeMatcher } from "./adapter.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type UnchainedCore } from '@unchainedshop/core';
|
|
2
|
+
import type { GridFSFileUploadsModule } from './module.ts';
|
|
3
|
+
export declare const gridfsRouteHandler: (request: Request, context: UnchainedCore & {
|
|
4
|
+
params: Record<string, string>;
|
|
5
|
+
modules: GridFSFileUploadsModule;
|
|
6
|
+
rawRequest?: any;
|
|
7
|
+
}) => Promise<Response>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Readable, PassThrough } from 'node:stream';
|
|
2
|
+
import { finished, pipeline } from 'node:stream/promises';
|
|
3
|
+
import { buildHashedFilename } from '@unchainedshop/core-files';
|
|
4
|
+
import sign from "./sign.js";
|
|
5
|
+
import { getFileAdapter } from '@unchainedshop/core';
|
|
6
|
+
import { createLogger } from '@unchainedshop/logger';
|
|
7
|
+
import { timingSafeStringEqual } from '@unchainedshop/utils';
|
|
8
|
+
const { GRIDFS_PUT_SERVER_PATH = '/gridfs' } = process.env;
|
|
9
|
+
const logger = createLogger('unchained:gridfs');
|
|
10
|
+
export const gridfsRouteHandler = async (request, context) => {
|
|
11
|
+
try {
|
|
12
|
+
const { services, modules, params } = context;
|
|
13
|
+
const directoryName = decodeURIComponent(params.directoryName);
|
|
14
|
+
const fileName = decodeURIComponent(params.fileName);
|
|
15
|
+
const corsHeaders = {
|
|
16
|
+
'Access-Control-Allow-Methods': 'GET, PUT, OPTIONS',
|
|
17
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
18
|
+
'Access-Control-Allow-Origin': '*',
|
|
19
|
+
};
|
|
20
|
+
if (request.method === 'OPTIONS') {
|
|
21
|
+
return new Response(null, {
|
|
22
|
+
status: 200,
|
|
23
|
+
headers: corsHeaders,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (request.method === 'PUT') {
|
|
27
|
+
const url = new URL(request.url);
|
|
28
|
+
const signature = url.searchParams.get('s');
|
|
29
|
+
const expiryTimestamp = url.searchParams.get('e');
|
|
30
|
+
if (!signature || !expiryTimestamp) {
|
|
31
|
+
return new Response(JSON.stringify({ error: 'Missing signature or expiry' }), {
|
|
32
|
+
status: 400,
|
|
33
|
+
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const expiryDate = new Date(parseInt(expiryTimestamp, 10));
|
|
37
|
+
const fileId = await buildHashedFilename(directoryName, fileName, expiryDate);
|
|
38
|
+
const expectedSignature = await sign(directoryName, fileId, expiryDate.getTime());
|
|
39
|
+
if (!(await timingSafeStringEqual(expectedSignature, signature))) {
|
|
40
|
+
logger.error('Invalid signature', { fileId, expiryDate });
|
|
41
|
+
return new Response(JSON.stringify({ error: 'Invalid signature' }), {
|
|
42
|
+
status: 403,
|
|
43
|
+
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const file = await modules.files.findFile({ fileId });
|
|
47
|
+
if (!file) {
|
|
48
|
+
logger.error('File not found', { fileId });
|
|
49
|
+
return new Response(JSON.stringify({ error: 'File not found' }), {
|
|
50
|
+
status: 404,
|
|
51
|
+
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (file.expires === null) {
|
|
55
|
+
logger.error('File already linked', { fileId });
|
|
56
|
+
return new Response(JSON.stringify({ error: 'File already linked' }), {
|
|
57
|
+
status: 400,
|
|
58
|
+
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const contentType = request.headers.get('Content-Type');
|
|
62
|
+
const type = contentType === 'application/octet-stream' ? file.type || contentType : contentType || file.type;
|
|
63
|
+
const writeStream = await modules.gridfsFileUploads.createWriteStream(directoryName, fileId, fileName, {
|
|
64
|
+
chunkSizeBytes: 1024 * 1024,
|
|
65
|
+
metadata: { 'content-type': type },
|
|
66
|
+
});
|
|
67
|
+
const bodyStream = request.body;
|
|
68
|
+
if (!bodyStream) {
|
|
69
|
+
throw new Error('No request body');
|
|
70
|
+
}
|
|
71
|
+
await pipeline(bodyStream, new PassThrough({ highWaterMark: 1024 * 1024 * 4 }), writeStream);
|
|
72
|
+
await finished(writeStream, { readable: false });
|
|
73
|
+
const { length } = writeStream;
|
|
74
|
+
await services.files.linkFile({ fileId, size: length, type });
|
|
75
|
+
return new Response(JSON.stringify({
|
|
76
|
+
success: true,
|
|
77
|
+
fileId,
|
|
78
|
+
size: length,
|
|
79
|
+
type,
|
|
80
|
+
}), {
|
|
81
|
+
status: 200,
|
|
82
|
+
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (request.method === 'GET') {
|
|
86
|
+
const url = new URL(request.url);
|
|
87
|
+
const signature = url.searchParams.get('s');
|
|
88
|
+
const expiryTimestamp = url.searchParams.get('e');
|
|
89
|
+
const fileDocument = await modules.files.findFile({
|
|
90
|
+
url: `${GRIDFS_PUT_SERVER_PATH}/${directoryName}/${fileName}`,
|
|
91
|
+
});
|
|
92
|
+
if (!fileDocument) {
|
|
93
|
+
logger.error('File not found', { fileName });
|
|
94
|
+
return new Response(null, {
|
|
95
|
+
status: 404,
|
|
96
|
+
headers: corsHeaders,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (fileDocument?.meta?.isPrivate) {
|
|
100
|
+
if (!expiryTimestamp || !signature) {
|
|
101
|
+
return new Response(JSON.stringify({ error: 'Missing signature or expiry' }), {
|
|
102
|
+
status: 403,
|
|
103
|
+
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const expiry = parseInt(expiryTimestamp, 10);
|
|
107
|
+
if (expiry <= Date.now()) {
|
|
108
|
+
logger.error('URL Expired', { fileName, expiry });
|
|
109
|
+
return new Response(JSON.stringify({ error: 'Access restricted: Expired.' }), {
|
|
110
|
+
status: 403,
|
|
111
|
+
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const fileUploadAdapter = getFileAdapter();
|
|
115
|
+
const signedUrl = await fileUploadAdapter.createDownloadURL(fileDocument, expiry);
|
|
116
|
+
const expectedSignature = signedUrl ? new URL(signedUrl, 'file://').searchParams.get('s') : null;
|
|
117
|
+
if (!signedUrl ||
|
|
118
|
+
!expectedSignature ||
|
|
119
|
+
!(await timingSafeStringEqual(expectedSignature, signature))) {
|
|
120
|
+
logger.error('Invalid signature', { fileName, expiry });
|
|
121
|
+
return new Response(JSON.stringify({ error: 'Access restricted: Invalid signature.' }), {
|
|
122
|
+
status: 403,
|
|
123
|
+
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const file = await modules.gridfsFileUploads.getFileInfo(directoryName, fileDocument._id);
|
|
128
|
+
const readStream = await modules.gridfsFileUploads.createReadStream(directoryName, fileDocument._id);
|
|
129
|
+
const webStream = Readable.toWeb(readStream);
|
|
130
|
+
const headers = {
|
|
131
|
+
...corsHeaders,
|
|
132
|
+
};
|
|
133
|
+
if (file?.metadata?.['content-type']) {
|
|
134
|
+
headers['Content-Type'] = file.metadata['content-type'];
|
|
135
|
+
}
|
|
136
|
+
if (file?.length) {
|
|
137
|
+
headers['Content-Length'] = file.length.toString();
|
|
138
|
+
}
|
|
139
|
+
return new Response(webStream, {
|
|
140
|
+
status: 200,
|
|
141
|
+
headers,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return new Response(null, {
|
|
145
|
+
status: 404,
|
|
146
|
+
headers: corsHeaders,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
logger.error(e);
|
|
151
|
+
if (e.code === 'ENOENT') {
|
|
152
|
+
return new Response(null, { status: 404 });
|
|
153
|
+
}
|
|
154
|
+
return new Response(JSON.stringify({
|
|
155
|
+
error: e.message || 'Internal Server Error',
|
|
156
|
+
name: e.name,
|
|
157
|
+
code: e.code,
|
|
158
|
+
}), {
|
|
159
|
+
status: 500,
|
|
160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { FileAdapter, buildHashedFilename, resolveExpirationDate, expiryOffsetInMs, } from '@unchainedshop/core-files';
|
|
3
|
+
import mime from 'mime/lite';
|
|
4
|
+
import { Client } from 'minio';
|
|
5
|
+
import { AssumeRoleProvider } from 'minio/dist/esm/AssumeRoleProvider.mjs';
|
|
6
|
+
import { createLogger } from '@unchainedshop/logger';
|
|
7
|
+
const logger = createLogger('unchained:minio');
|
|
8
|
+
const { MINIO_ACCESS_KEY, MINIO_REGION, MINIO_STS_ENDPOINT, MINIO_SECRET_KEY, MINIO_ENDPOINT, MINIO_BUCKET_NAME, MINIO_UPLOAD_PREFIX, NODE_ENV, AMAZON_S3_SESSION_TOKEN, } = process.env;
|
|
9
|
+
let client = null;
|
|
10
|
+
export async function connectToMinio() {
|
|
11
|
+
if (!MINIO_ENDPOINT || !MINIO_BUCKET_NAME) {
|
|
12
|
+
logger.error('Please configure Minio/S3 by providing MINIO_ENDPOINT & MINIO_BUCKET_NAME to use upload features');
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const resolvedUrl = new URL(MINIO_ENDPOINT);
|
|
17
|
+
const minioClient = new Client({
|
|
18
|
+
endPoint: resolvedUrl.hostname,
|
|
19
|
+
useSSL: resolvedUrl.protocol === 'https:',
|
|
20
|
+
port: parseInt(resolvedUrl.port, 10) || undefined,
|
|
21
|
+
region: MINIO_REGION,
|
|
22
|
+
sessionToken: AMAZON_S3_SESSION_TOKEN,
|
|
23
|
+
accessKey: MINIO_ACCESS_KEY,
|
|
24
|
+
secretKey: MINIO_SECRET_KEY,
|
|
25
|
+
});
|
|
26
|
+
if (NODE_ENV === 'development')
|
|
27
|
+
minioClient?.traceOn(process.stdout);
|
|
28
|
+
if (MINIO_STS_ENDPOINT) {
|
|
29
|
+
const ap = new AssumeRoleProvider({
|
|
30
|
+
stsEndpoint: MINIO_STS_ENDPOINT,
|
|
31
|
+
accessKey: MINIO_ACCESS_KEY,
|
|
32
|
+
secretKey: MINIO_SECRET_KEY,
|
|
33
|
+
});
|
|
34
|
+
await minioClient.setCredentialsProvider(ap);
|
|
35
|
+
}
|
|
36
|
+
return minioClient;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
logger.error(`Exception while creating Minio client: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const generateMinioPath = (directoryName, fileName) => {
|
|
44
|
+
const prefix = MINIO_UPLOAD_PREFIX || '';
|
|
45
|
+
return [prefix, directoryName, fileName].filter(Boolean).join('/');
|
|
46
|
+
};
|
|
47
|
+
const generateMinioUrl = (directoryName, hashedFilename) => {
|
|
48
|
+
return `${MINIO_ENDPOINT}/${MINIO_BUCKET_NAME}/${generateMinioPath(directoryName, hashedFilename)}`;
|
|
49
|
+
};
|
|
50
|
+
connectToMinio().then(function setClient(c) {
|
|
51
|
+
client = c || client;
|
|
52
|
+
});
|
|
53
|
+
const getObjectStats = async (fileName) => {
|
|
54
|
+
if (!client)
|
|
55
|
+
throw new Error('Minio not connected, check env variables');
|
|
56
|
+
return client.statObject(MINIO_BUCKET_NAME, fileName);
|
|
57
|
+
};
|
|
58
|
+
const bufferToStream = (buffer) => {
|
|
59
|
+
const stream = new Readable();
|
|
60
|
+
stream.push(buffer);
|
|
61
|
+
stream.push(null);
|
|
62
|
+
return stream;
|
|
63
|
+
};
|
|
64
|
+
export const MinioAdapter = {
|
|
65
|
+
key: 'shop.unchained.file-upload-plugin.minio',
|
|
66
|
+
label: 'Uploads files into an S3 bucket using minio',
|
|
67
|
+
version: '1.1.0',
|
|
68
|
+
...FileAdapter,
|
|
69
|
+
async createDownloadURL(file) {
|
|
70
|
+
if (file.meta?.isPrivate)
|
|
71
|
+
throw new Error("Minio Plugin doesn't support private files yet");
|
|
72
|
+
return generateMinioUrl(file.path, file._id);
|
|
73
|
+
},
|
|
74
|
+
async createSignedURL(directoryName, fileName) {
|
|
75
|
+
if (!client)
|
|
76
|
+
throw new Error('Minio not connected, check env variables');
|
|
77
|
+
const expiryDate = resolveExpirationDate();
|
|
78
|
+
const _id = await buildHashedFilename(directoryName, fileName, expiryDate);
|
|
79
|
+
const url = await client.presignedPutObject(MINIO_BUCKET_NAME, generateMinioPath(directoryName, _id), expiryOffsetInMs() / 1000);
|
|
80
|
+
return {
|
|
81
|
+
_id,
|
|
82
|
+
directoryName,
|
|
83
|
+
expiryDate,
|
|
84
|
+
fileName,
|
|
85
|
+
type: mime.getType(fileName),
|
|
86
|
+
putURL: url,
|
|
87
|
+
url: generateMinioUrl(directoryName, _id),
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
async removeFiles(files) {
|
|
91
|
+
if (!client)
|
|
92
|
+
throw new Error('Minio not connected, check env variables');
|
|
93
|
+
const fileIds = files.map(({ path, _id }) => {
|
|
94
|
+
return `${path}/${_id}`;
|
|
95
|
+
});
|
|
96
|
+
await client.removeObjects(MINIO_BUCKET_NAME, fileIds);
|
|
97
|
+
},
|
|
98
|
+
async uploadFileFromStream(directoryName, rawFile) {
|
|
99
|
+
if (!client)
|
|
100
|
+
throw new Error('Minio not connected, check env variables');
|
|
101
|
+
let stream;
|
|
102
|
+
let fileName;
|
|
103
|
+
if (rawFile instanceof Promise) {
|
|
104
|
+
const { filename: fname, createReadStream } = await rawFile;
|
|
105
|
+
fileName = fname;
|
|
106
|
+
stream = createReadStream();
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
fileName = rawFile.filename;
|
|
110
|
+
stream = bufferToStream(Buffer.from(rawFile.buffer, 'base64'));
|
|
111
|
+
}
|
|
112
|
+
const _id = await buildHashedFilename(directoryName, fileName, new Date());
|
|
113
|
+
const type = mime.getType(fileName) || (await Promise.resolve(rawFile)).mimetype;
|
|
114
|
+
const metaData = {
|
|
115
|
+
'Content-Type': type,
|
|
116
|
+
};
|
|
117
|
+
await client.putObject(MINIO_BUCKET_NAME, generateMinioPath(directoryName, _id), stream, undefined, metaData);
|
|
118
|
+
const { size } = await getObjectStats(generateMinioPath(directoryName, _id));
|
|
119
|
+
return {
|
|
120
|
+
_id,
|
|
121
|
+
directoryName,
|
|
122
|
+
expiryDate: null,
|
|
123
|
+
fileName,
|
|
124
|
+
size,
|
|
125
|
+
type,
|
|
126
|
+
url: generateMinioUrl(directoryName, _id),
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
async uploadFileFromURL(directoryName, { fileLink, fileName: fname, fileId, headers }) {
|
|
130
|
+
if (!client)
|
|
131
|
+
throw new Error('Minio not connected, check env variables');
|
|
132
|
+
const { href } = new URL(fileLink);
|
|
133
|
+
const fileName = fname || href.split('/').pop();
|
|
134
|
+
const hashedFilename = await buildHashedFilename(directoryName, fileName, new Date());
|
|
135
|
+
const url = new URL(fileLink);
|
|
136
|
+
const response = await fetch(url, { headers });
|
|
137
|
+
const type = mime.getType(fileName) || response.headers['content-type'];
|
|
138
|
+
const readable = Readable.fromWeb(response.body);
|
|
139
|
+
const metaData = {
|
|
140
|
+
'Content-Type': type,
|
|
141
|
+
};
|
|
142
|
+
await client.putObject(MINIO_BUCKET_NAME, generateMinioPath(directoryName, hashedFilename), readable, undefined, metaData);
|
|
143
|
+
const { size } = await getObjectStats(generateMinioPath(directoryName, hashedFilename));
|
|
144
|
+
return {
|
|
145
|
+
_id: fileId || hashedFilename,
|
|
146
|
+
directoryName,
|
|
147
|
+
expiryDate: null,
|
|
148
|
+
fileName,
|
|
149
|
+
size,
|
|
150
|
+
type,
|
|
151
|
+
url: generateMinioUrl(directoryName, hashedFilename),
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
async createDownloadStream(file) {
|
|
155
|
+
if (!client)
|
|
156
|
+
throw new Error('Minio not connected, check env variables');
|
|
157
|
+
const stream = await client.getObject(MINIO_BUCKET_NAME, generateMinioPath(file.path, file._id));
|
|
158
|
+
return stream;
|
|
159
|
+
},
|
|
160
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
export async function minioWebhookHandler(request, context) {
|
|
14
|
+
try {
|
|
15
|
+
const authorization = request.headers.get('authorization') || '';
|
|
16
|
+
if (!(await isAuthorized(authorization))) {
|
|
17
|
+
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
|
18
|
+
status: 401,
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const body = await request.json();
|
|
23
|
+
const { Records = [], EventName } = body;
|
|
24
|
+
if (EventName === 's3:ObjectCreated:Put') {
|
|
25
|
+
const [{ s3 }] = Records;
|
|
26
|
+
const { object } = s3;
|
|
27
|
+
const { size, contentType: type } = object;
|
|
28
|
+
const [fileId] = object.key.split('.');
|
|
29
|
+
await context.services.files.linkFile({ fileId, type, size });
|
|
30
|
+
return new Response(null, { status: 200 });
|
|
31
|
+
}
|
|
32
|
+
return new Response(null, { status: 404 });
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
logger.error(e);
|
|
36
|
+
return new Response(JSON.stringify({ name: e.name, code: e.code, message: e.message }), {
|
|
37
|
+
status: 503,
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|