@hartl-services/medusa-base 2.6.0 → 2.7.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/.medusa/server/src/admin/admin-components/features/plugin-features/components/ImageSettingInput.d.ts +3 -2
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ImageSettingInput.js +4 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginActionsSection.js +4 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginBreadcrumb.d.ts +16 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginBreadcrumb.js +25 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginDetailPage.js +1 -1
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeatureSections.d.ts +5 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeatureSections.js +8 -5
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.d.ts +4 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.js +24 -27
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.d.ts +2 -2
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.js +9 -9
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.d.ts +5 -1
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.js +11 -15
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginPageShell.d.ts +10 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginPageShell.js +10 -5
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.d.ts +0 -1
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.js +73 -26
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.d.ts +5 -1
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.js +8 -6
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.d.ts +11 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.js +15 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +36 -5
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +144 -20
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +36 -5
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.d.ts +15 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.js +66 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +144 -20
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.d.ts +49 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.js +72 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.d.ts +1 -12
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.js +4 -20
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.d.ts +43 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.js +66 -0
- package/.medusa/server/src/admin/admin-components/i18n/texts.d.ts +22 -2
- package/.medusa/server/src/admin/admin-components/i18n/texts.js +29 -7
- package/.medusa/server/src/admin/admin-components/index.d.ts +12 -0
- package/.medusa/server/src/admin/admin-components/index.js +11 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/AdminPageHeader.d.ts +22 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/AdminPageHeader.js +8 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/AdminSection.d.ts +26 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/AdminSection.js +12 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/BreadcrumbTrail.d.ts +23 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/BreadcrumbTrail.js +20 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/FileDropField.d.ts +21 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/FileDropField.js +33 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/FocusModalShell.d.ts +27 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/FocusModalShell.js +13 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/InfoTooltip.d.ts +13 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/InfoTooltip.js +8 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/MultiSelectDropdown.d.ts +22 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/MultiSelectDropdown.js +14 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/PdfPreviewModal.js +1 -1
- package/.medusa/server/src/admin/admin-components/shared/ui/PluginIcon.d.ts +10 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/PluginIcon.js +10 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/SwitchRow.d.ts +27 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/SwitchRow.js +15 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/WidgetHeader.d.ts +15 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/WidgetHeader.js +8 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/file-drop.d.ts +13 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/file-drop.js +16 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/multi-select.d.ts +13 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/multi-select.js +11 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/plugin-icon.d.ts +12 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/plugin-icon.js +29 -0
- package/.medusa/server/src/admin/index.js +3123 -2019
- package/.medusa/server/src/admin/index.mjs +3126 -2022
- package/.medusa/server/src/api/admin-first-login-password-policy.js +56 -0
- package/.medusa/server/src/api/answer-actor-type-casing-not-found.js +26 -0
- package/.medusa/server/src/api/answer-code-provider-route-not-found.js +34 -0
- package/.medusa/server/src/api/code-account-password-routes.js +92 -0
- package/.medusa/server/src/api/middlewares.js +82 -17
- package/.medusa/server/src/api/password-policy.js +55 -10
- package/.medusa/server/src/api/require-login-code-customer-email.js +40 -0
- package/.medusa/server/src/api/store/customer-auth/login-code/route.js +17 -0
- package/.medusa/server/src/api/store/customer-auth/password-policy/route.js +19 -0
- package/.medusa/server/src/customer-auth/application/prepare-code-account-password-reset.js +111 -0
- package/.medusa/server/src/customer-auth/application/request-login-code.js +145 -0
- package/.medusa/server/src/customer-auth/customer-auth-methods-snapshot.js +55 -0
- package/.medusa/server/src/customer-auth/customer-password-policy.js +40 -0
- package/.medusa/server/src/customer-auth/domain/account-linking.js +53 -0
- package/.medusa/server/src/customer-auth/domain/auth-methods.js +32 -0
- package/.medusa/server/src/customer-auth/domain/login-code.js +39 -0
- package/.medusa/server/src/customer-auth/login-code-accounts.js +146 -0
- package/.medusa/server/src/customer-auth/login-code-state.js +139 -0
- package/.medusa/server/src/customer-auth/plugin-features.js +80 -0
- package/.medusa/server/src/customer-auth/remove-provider-identity.js +18 -0
- package/.medusa/server/src/customer-auth/start-customer-auth-methods.js +20 -0
- package/.medusa/server/src/documents/plugin-features.js +8 -2
- package/.medusa/server/src/email/application/deliver-ephemeral-auth-mail.js +186 -0
- package/.medusa/server/src/email/application/deliver-login-code.js +89 -0
- package/.medusa/server/src/email/application/handle-customer-verification-event.js +29 -168
- package/.medusa/server/src/email/application/handle-password-reset-event.js +27 -166
- package/.medusa/server/src/email/customer-verification-snapshot.js +20 -108
- package/.medusa/server/src/email/domain/business-keys.js +15 -2
- package/.medusa/server/src/email/plugin-features.js +8 -3
- package/.medusa/server/src/email/rendering/default-templates.js +2 -1
- package/.medusa/server/src/email/templates/blocks/dynamic-blocks.js +19 -1
- package/.medusa/server/src/email/templates/defaults/documents.js +7 -1
- package/.medusa/server/src/email/templates/defaults/subjects.js +3 -1
- package/.medusa/server/src/email/templates/defaults/texts.js +5 -2
- package/.medusa/server/src/email/templates/render.js +2 -1
- package/.medusa/server/src/email/templates/sample-data.js +2 -1
- package/.medusa/server/src/email/templates/validate.js +8 -3
- package/.medusa/server/src/email/types/email-kinds.js +4 -2
- package/.medusa/server/src/email/types/template-data.js +9 -1
- package/.medusa/server/src/modules/customer-auth/index.js +14 -0
- package/.medusa/server/src/modules/customer-auth/service.js +35 -0
- package/.medusa/server/src/modules/email/migrations/Migration20261010090000.js +20 -0
- package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +4 -1
- package/.medusa/server/src/modules/plugin-features/service.js +26 -1
- package/.medusa/server/src/providers/email-otp/index.js +8 -0
- package/.medusa/server/src/providers/email-otp/service.js +96 -0
- package/.medusa/server/src/public/descriptor.js +117 -5
- package/.medusa/server/src/public/effective-features.js +22 -1
- package/.medusa/server/src/public/index.js +4 -2
- package/.medusa/server/src/public/plugin-icons.js +28 -0
- package/.medusa/server/src/public/shop-master-data.js +33 -14
- package/.medusa/server/src/shared/lib/index.js +2 -1
- package/.medusa/server/src/shared/lib/refreshing-snapshot.js +122 -0
- package/.medusa/server/src/shop-config/build-food-shop-config.js +38 -1
- package/README.md +436 -87
- package/dist/customer-auth/customer-auth-methods-snapshot.d.ts +25 -0
- package/dist/customer-auth/customer-auth-methods-snapshot.js +54 -0
- package/dist/customer-auth/domain/auth-methods.d.ts +16 -0
- package/dist/customer-auth/domain/auth-methods.js +31 -0
- package/dist/customer-auth/plugin-features.d.ts +2 -0
- package/dist/customer-auth/plugin-features.js +79 -0
- package/dist/email/customer-verification-snapshot.d.ts +3 -15
- package/dist/email/customer-verification-snapshot.js +19 -107
- package/dist/email/types/email-kinds.d.ts +4 -3
- package/dist/email/types/email-kinds.js +3 -1
- package/dist/email/types/template-data.d.ts +8 -0
- package/dist/email/types/template-data.js +8 -0
- package/dist/public/descriptor.js +116 -4
- package/dist/public/effective-features.d.ts +10 -0
- package/dist/public/effective-features.js +21 -0
- package/dist/public/index.d.ts +2 -1
- package/dist/public/index.js +3 -1
- package/dist/public/plugin-icons.d.ts +7 -0
- package/dist/public/plugin-icons.js +27 -0
- package/dist/public/shop-master-data.js +32 -13
- package/dist/public/types.d.ts +63 -1
- package/dist/shared/lib/index.d.ts +1 -0
- package/dist/shared/lib/index.js +1 -0
- package/dist/shared/lib/refreshing-snapshot.d.ts +52 -0
- package/dist/shared/lib/refreshing-snapshot.js +121 -0
- package/dist/shop-config/build-food-shop-config.js +37 -0
- package/package.json +8 -3
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasCustomerAccountIdentity = exports.findCodeAccountAuthIdentityId = exports.findPasswordIdentities = exports.deleteUnconfirmedPasswordIdentities = exports.findEmailPasswordAccounts = exports.findOwnLoginIdentity = void 0;
|
|
4
|
+
/** The address's own code-login identity (provider `email-otp`), or null. */
|
|
5
|
+
const findOwnLoginIdentity = async (sql, entityId) => {
|
|
6
|
+
const result = await sql.raw(`SELECT pi.id AS provider_identity_id,
|
|
7
|
+
pi.auth_identity_id AS auth_identity_id,
|
|
8
|
+
NULLIF(ai.app_metadata->>'customer_id', '') AS customer_id
|
|
9
|
+
FROM provider_identity pi
|
|
10
|
+
JOIN auth_identity ai ON ai.id = pi.auth_identity_id AND ai.deleted_at IS NULL
|
|
11
|
+
WHERE pi.provider = 'email-otp'
|
|
12
|
+
AND pi.entity_id = ?
|
|
13
|
+
AND pi.deleted_at IS NULL`, [entityId]);
|
|
14
|
+
const row = result.rows[0];
|
|
15
|
+
return row
|
|
16
|
+
? {
|
|
17
|
+
providerIdentityId: String(row.provider_identity_id),
|
|
18
|
+
authIdentityId: String(row.auth_identity_id),
|
|
19
|
+
customerId: typeof row.customer_id === "string" && row.customer_id
|
|
20
|
+
? row.customer_id
|
|
21
|
+
: null,
|
|
22
|
+
}
|
|
23
|
+
: null;
|
|
24
|
+
};
|
|
25
|
+
exports.findOwnLoginIdentity = findOwnLoginIdentity;
|
|
26
|
+
/**
|
|
27
|
+
* A verified Medusa auth verification of the provider identity `pi`'s very
|
|
28
|
+
* address (`entity_type` "email"): the one confirmation check of the linking
|
|
29
|
+
* rule and of the clean-up after a code login.
|
|
30
|
+
*/
|
|
31
|
+
const ADDRESS_CONFIRMED_SQL = `EXISTS (
|
|
32
|
+
SELECT 1 FROM auth_verification v
|
|
33
|
+
WHERE v.auth_identity_id = pi.auth_identity_id
|
|
34
|
+
AND v.entity_id = pi.entity_id
|
|
35
|
+
AND v.entity_type = 'email'
|
|
36
|
+
AND v.verified_at IS NOT NULL
|
|
37
|
+
AND v.deleted_at IS NULL
|
|
38
|
+
)`;
|
|
39
|
+
/**
|
|
40
|
+
* Customer accounts that registered with this address through `emailpass`.
|
|
41
|
+
* `emailpass` stores the address exactly as typed, so the match ignores
|
|
42
|
+
* letter case. Only identities whose auth identity carries a customer count
|
|
43
|
+
* (`app_metadata.customer_id`); `confirmed` is a verified Medusa auth
|
|
44
|
+
* verification of that very address. `customerEmailMatches` says the customer
|
|
45
|
+
* record itself (not deleted) has the same address, so a password identity
|
|
46
|
+
* pointing at a customer of another address is never linked.
|
|
47
|
+
*/
|
|
48
|
+
const findEmailPasswordAccounts = async (sql, normalizedEmail) => {
|
|
49
|
+
const result = await sql.raw(`SELECT pi.auth_identity_id AS auth_identity_id,
|
|
50
|
+
pi.entity_id AS entity_id,
|
|
51
|
+
${ADDRESS_CONFIRMED_SQL} AS confirmed,
|
|
52
|
+
(c.id IS NOT NULL AND lower(c.email) = ?) AS customer_email_matches
|
|
53
|
+
FROM provider_identity pi
|
|
54
|
+
JOIN auth_identity ai ON ai.id = pi.auth_identity_id AND ai.deleted_at IS NULL
|
|
55
|
+
LEFT JOIN customer c
|
|
56
|
+
ON c.id = ai.app_metadata->>'customer_id' AND c.deleted_at IS NULL
|
|
57
|
+
WHERE pi.provider = 'emailpass'
|
|
58
|
+
AND lower(pi.entity_id) = ?
|
|
59
|
+
AND pi.entity_id ~ '^[\\x21-\\x7e]+$'
|
|
60
|
+
AND pi.deleted_at IS NULL
|
|
61
|
+
AND NULLIF(ai.app_metadata->>'customer_id', '') IS NOT NULL`, [normalizedEmail, normalizedEmail]);
|
|
62
|
+
return result.rows.map((row) => ({
|
|
63
|
+
authIdentityId: String(row.auth_identity_id),
|
|
64
|
+
entityId: String(row.entity_id),
|
|
65
|
+
confirmed: row.confirmed === true,
|
|
66
|
+
customerEmailMatches: row.customer_email_matches === true,
|
|
67
|
+
}));
|
|
68
|
+
};
|
|
69
|
+
exports.findEmailPasswordAccounts = findEmailPasswordAccounts;
|
|
70
|
+
/**
|
|
71
|
+
* Deletes the `emailpass` identities of an auth identity whose address was
|
|
72
|
+
* never confirmed, for good (the unique index on `(entity_id, provider)` has no
|
|
73
|
+
* `deleted_at` filter, so a soft delete would block a new registration). A
|
|
74
|
+
* password somebody else may have planted for the address before the code
|
|
75
|
+
* login took over can then never be used. Returns how many were deleted.
|
|
76
|
+
*/
|
|
77
|
+
const deleteUnconfirmedPasswordIdentities = async (sql, authIdentityId) => {
|
|
78
|
+
const result = await sql.raw(`DELETE FROM provider_identity pi
|
|
79
|
+
WHERE pi.auth_identity_id = ?
|
|
80
|
+
AND pi.provider = 'emailpass'
|
|
81
|
+
AND NOT ${ADDRESS_CONFIRMED_SQL}
|
|
82
|
+
RETURNING pi.id`, [authIdentityId]);
|
|
83
|
+
return result.rows.length;
|
|
84
|
+
};
|
|
85
|
+
exports.deleteUnconfirmedPasswordIdentities = deleteUnconfirmedPasswordIdentities;
|
|
86
|
+
/**
|
|
87
|
+
* Every `emailpass` identity of the address, letter case ignored (`emailpass`
|
|
88
|
+
* stores the address as typed), with or without a customer.
|
|
89
|
+
*/
|
|
90
|
+
const findPasswordIdentities = async (sql, normalizedEmail) => {
|
|
91
|
+
const result = await sql.raw(`SELECT pi.id AS provider_identity_id,
|
|
92
|
+
pi.auth_identity_id AS auth_identity_id,
|
|
93
|
+
pi.entity_id AS entity_id,
|
|
94
|
+
NULLIF(ai.app_metadata->>'customer_id', '') AS customer_id
|
|
95
|
+
FROM provider_identity pi
|
|
96
|
+
JOIN auth_identity ai ON ai.id = pi.auth_identity_id AND ai.deleted_at IS NULL
|
|
97
|
+
WHERE pi.provider = 'emailpass'
|
|
98
|
+
AND lower(pi.entity_id) = ?
|
|
99
|
+
AND pi.entity_id ~ '^[\\x21-\\x7e]+$'
|
|
100
|
+
AND pi.deleted_at IS NULL`, [normalizedEmail]);
|
|
101
|
+
return result.rows.map((row) => ({
|
|
102
|
+
providerIdentityId: String(row.provider_identity_id),
|
|
103
|
+
authIdentityId: String(row.auth_identity_id),
|
|
104
|
+
entityId: String(row.entity_id),
|
|
105
|
+
customerId: typeof row.customer_id === "string" && row.customer_id
|
|
106
|
+
? row.customer_id
|
|
107
|
+
: null,
|
|
108
|
+
}));
|
|
109
|
+
};
|
|
110
|
+
exports.findPasswordIdentities = findPasswordIdentities;
|
|
111
|
+
/**
|
|
112
|
+
* The auth identity of the address's code login (`email-otp`) when it carries a
|
|
113
|
+
* customer (not deleted) with that very address: the account the address was
|
|
114
|
+
* proven for. Null otherwise.
|
|
115
|
+
*/
|
|
116
|
+
const findCodeAccountAuthIdentityId = async (sql, normalizedEmail) => {
|
|
117
|
+
const result = await sql.raw(`SELECT pi.auth_identity_id AS auth_identity_id
|
|
118
|
+
FROM provider_identity pi
|
|
119
|
+
JOIN auth_identity ai ON ai.id = pi.auth_identity_id AND ai.deleted_at IS NULL
|
|
120
|
+
JOIN customer c
|
|
121
|
+
ON c.id = ai.app_metadata->>'customer_id' AND c.deleted_at IS NULL
|
|
122
|
+
WHERE pi.provider = 'email-otp'
|
|
123
|
+
AND pi.entity_id = ?
|
|
124
|
+
AND pi.deleted_at IS NULL
|
|
125
|
+
AND lower(c.email) = ?`, [normalizedEmail, normalizedEmail]);
|
|
126
|
+
const id = result.rows[0]?.auth_identity_id;
|
|
127
|
+
return typeof id === "string" && id ? id : null;
|
|
128
|
+
};
|
|
129
|
+
exports.findCodeAccountAuthIdentityId = findCodeAccountAuthIdentityId;
|
|
130
|
+
/**
|
|
131
|
+
* Whether an identity of any provider for the address (letter case ignored)
|
|
132
|
+
* belongs to an auth identity that carries a customer.
|
|
133
|
+
*/
|
|
134
|
+
const hasCustomerAccountIdentity = async (sql, normalizedEmail) => {
|
|
135
|
+
const result = await sql.raw(`SELECT 1 AS found
|
|
136
|
+
FROM provider_identity pi
|
|
137
|
+
JOIN auth_identity ai ON ai.id = pi.auth_identity_id AND ai.deleted_at IS NULL
|
|
138
|
+
WHERE lower(pi.entity_id) = ?
|
|
139
|
+
AND pi.entity_id ~ '^[\\x21-\\x7e]+$'
|
|
140
|
+
AND pi.deleted_at IS NULL
|
|
141
|
+
AND NULLIF(ai.app_metadata->>'customer_id', '') IS NOT NULL
|
|
142
|
+
LIMIT 1`, [normalizedEmail]);
|
|
143
|
+
return result.rows.length > 0;
|
|
144
|
+
};
|
|
145
|
+
exports.hasCustomerAccountIdentity = hasCustomerAccountIdentity;
|
|
146
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9naW4tY29kZS1hY2NvdW50cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9jdXN0b21lci1hdXRoL2xvZ2luLWNvZGUtYWNjb3VudHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBTUEsNkVBQTZFO0FBQ3RFLE1BQU0sb0JBQW9CLEdBQUcsS0FBSyxFQUN2QyxHQUFpQixFQUNqQixRQUFnQixFQUNrQixFQUFFO0lBQ3BDLE1BQU0sTUFBTSxHQUFHLE1BQU0sR0FBRyxDQUFDLEdBQUcsQ0FDMUI7Ozs7Ozs7aUNBTzZCLEVBQzdCLENBQUMsUUFBUSxDQUFDLENBQ1gsQ0FBQTtJQUNELE1BQU0sR0FBRyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUE7SUFDMUIsT0FBTyxHQUFHO1FBQ1IsQ0FBQyxDQUFDO1lBQ0Usa0JBQWtCLEVBQUUsTUFBTSxDQUFDLEdBQUcsQ0FBQyxvQkFBb0IsQ0FBQztZQUNwRCxjQUFjLEVBQUUsTUFBTSxDQUFDLEdBQUcsQ0FBQyxnQkFBZ0IsQ0FBQztZQUM1QyxVQUFVLEVBQ1IsT0FBTyxHQUFHLENBQUMsV0FBVyxLQUFLLFFBQVEsSUFBSSxHQUFHLENBQUMsV0FBVztnQkFDcEQsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxXQUFXO2dCQUNqQixDQUFDLENBQUMsSUFBSTtTQUNYO1FBQ0gsQ0FBQyxDQUFDLElBQUksQ0FBQTtBQUNWLENBQUMsQ0FBQTtBQTFCWSxRQUFBLG9CQUFvQix3QkEwQmhDO0FBRUQ7Ozs7R0FJRztBQUNILE1BQU0scUJBQXFCLEdBQUc7Ozs7Ozs7RUFPNUIsQ0FBQTtBQUVGOzs7Ozs7OztHQVFHO0FBQ0ksTUFBTSx5QkFBeUIsR0FBRyxLQUFLLEVBQzVDLEdBQWlCLEVBQ2pCLGVBQXVCLEVBQ2MsRUFBRTtJQUN2QyxNQUFNLE1BQU0sR0FBRyxNQUFNLEdBQUcsQ0FBQyxHQUFHLENBQzFCOztjQUVVLHFCQUFxQjs7Ozs7Ozs7OzttRUFVZ0Ms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
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.consumeLoginCode = exports.countLoginCodeAttempt = exports.storeLoginCode = exports.LOGIN_CODE_RESEND_SLACK_SECONDS = void 0;
|
|
4
|
+
const customer_auth_1 = require("@hartl-services/medusa-food-supplements-contracts/schemas/customer-auth");
|
|
5
|
+
/**
|
|
6
|
+
* The code state lives in `provider_identity.provider_metadata` of the
|
|
7
|
+
* `email-otp` provider identity: `login_code` is `{ hash, expires_at,
|
|
8
|
+
* attempts, sent_at }`, `login_code_budget` is `{ window_start, issued }`
|
|
9
|
+
* (codes issued in the current 24 hour window; issuing a new code does not
|
|
10
|
+
* reset it, a successful login does). Every transition below is one SQL
|
|
11
|
+
* statement whose WHERE clause carries the rule, so concurrent requests cannot
|
|
12
|
+
* both pass it. Time is the database's `now()`, never the application's clock.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The storefront starts its resend countdown when it receives the 202, which
|
|
16
|
+
* is before `sent_at` is written; the spacing check tolerates that head start.
|
|
17
|
+
*/
|
|
18
|
+
exports.LOGIN_CODE_RESEND_SLACK_SECONDS = 5;
|
|
19
|
+
/** Length of the issuance window (fixed, counted from its first code). */
|
|
20
|
+
const LOGIN_CODE_BUDGET_WINDOW_HOURS = 24;
|
|
21
|
+
/** The budget window of the row is still running (`window_start` < 24 h ago). */
|
|
22
|
+
const BUDGET_WINDOW_ACTIVE_SQL = `COALESCE(
|
|
23
|
+
jsonb_typeof(provider_metadata->'login_code_budget') = 'object'
|
|
24
|
+
AND (provider_metadata->'login_code_budget'->>'window_start')::timestamptz
|
|
25
|
+
> now() - make_interval(hours => ${LOGIN_CODE_BUDGET_WINDOW_HOURS}),
|
|
26
|
+
false
|
|
27
|
+
)`;
|
|
28
|
+
/**
|
|
29
|
+
* Stores a new code unless one was sent within the resend interval or the
|
|
30
|
+
* address already got `LOGIN_CODE_MAX_PER_DAY` codes in the running 24 hour
|
|
31
|
+
* window. Both rules are checked and the code and the budget are written by
|
|
32
|
+
* one conditional UPDATE. An expired window starts a new one with this code.
|
|
33
|
+
*/
|
|
34
|
+
const storeLoginCode = async (sql, providerIdentityId, hash) => {
|
|
35
|
+
const result = await sql.raw(`UPDATE provider_identity
|
|
36
|
+
SET provider_metadata = jsonb_set(
|
|
37
|
+
jsonb_set(
|
|
38
|
+
COALESCE(provider_metadata, '{}'::jsonb),
|
|
39
|
+
'{login_code}',
|
|
40
|
+
jsonb_build_object(
|
|
41
|
+
'hash', ?::text,
|
|
42
|
+
'expires_at', to_jsonb(now() + make_interval(mins => ?)),
|
|
43
|
+
'attempts', 0,
|
|
44
|
+
'sent_at', to_jsonb(now())
|
|
45
|
+
)
|
|
46
|
+
),
|
|
47
|
+
'{login_code_budget}',
|
|
48
|
+
jsonb_build_object(
|
|
49
|
+
'window_start', CASE
|
|
50
|
+
WHEN ${BUDGET_WINDOW_ACTIVE_SQL}
|
|
51
|
+
THEN provider_metadata->'login_code_budget'->'window_start'
|
|
52
|
+
ELSE to_jsonb(now())
|
|
53
|
+
END,
|
|
54
|
+
'issued', CASE
|
|
55
|
+
WHEN ${BUDGET_WINDOW_ACTIVE_SQL}
|
|
56
|
+
THEN to_jsonb((provider_metadata->'login_code_budget'->>'issued')::int + 1)
|
|
57
|
+
ELSE to_jsonb(1)
|
|
58
|
+
END
|
|
59
|
+
)
|
|
60
|
+
),
|
|
61
|
+
updated_at = now()
|
|
62
|
+
WHERE id = ?
|
|
63
|
+
AND provider = 'email-otp'
|
|
64
|
+
AND deleted_at IS NULL
|
|
65
|
+
AND (
|
|
66
|
+
jsonb_typeof(provider_metadata->'login_code') IS DISTINCT FROM 'object'
|
|
67
|
+
OR (provider_metadata->'login_code'->>'sent_at')::timestamptz
|
|
68
|
+
< now() - make_interval(secs => ?)
|
|
69
|
+
)
|
|
70
|
+
AND (
|
|
71
|
+
NOT ${BUDGET_WINDOW_ACTIVE_SQL}
|
|
72
|
+
OR (provider_metadata->'login_code_budget'->>'issued')::int < ?
|
|
73
|
+
)
|
|
74
|
+
RETURNING id`, [
|
|
75
|
+
hash,
|
|
76
|
+
customer_auth_1.LOGIN_CODE_TTL_MINUTES,
|
|
77
|
+
providerIdentityId,
|
|
78
|
+
customer_auth_1.LOGIN_CODE_RESEND_INTERVAL_SECONDS - exports.LOGIN_CODE_RESEND_SLACK_SECONDS,
|
|
79
|
+
customer_auth_1.LOGIN_CODE_MAX_PER_DAY,
|
|
80
|
+
]);
|
|
81
|
+
if (result.rows.length === 1) {
|
|
82
|
+
return "stored";
|
|
83
|
+
}
|
|
84
|
+
// Only to tell the reason apart for the caller's log; the decision above
|
|
85
|
+
// was already made atomically.
|
|
86
|
+
const capped = await sql.raw(`SELECT id FROM provider_identity
|
|
87
|
+
WHERE id = ?
|
|
88
|
+
AND provider = 'email-otp'
|
|
89
|
+
AND deleted_at IS NULL
|
|
90
|
+
AND ${BUDGET_WINDOW_ACTIVE_SQL}
|
|
91
|
+
AND (provider_metadata->'login_code_budget'->>'issued')::int >= ?`, [providerIdentityId, customer_auth_1.LOGIN_CODE_MAX_PER_DAY]);
|
|
92
|
+
return capped.rows.length === 1 ? "capped" : "throttled";
|
|
93
|
+
};
|
|
94
|
+
exports.storeLoginCode = storeLoginCode;
|
|
95
|
+
/**
|
|
96
|
+
* Counts one attempt against the address' live code and returns the stored
|
|
97
|
+
* hash; null when there is no code, it is expired or its attempts are used
|
|
98
|
+
* up. The count happens before the comparison, so parallel guesses share the
|
|
99
|
+
* limit instead of each seeing the same remaining budget.
|
|
100
|
+
*/
|
|
101
|
+
const countLoginCodeAttempt = async (sql, entityId) => {
|
|
102
|
+
const result = await sql.raw(`UPDATE provider_identity
|
|
103
|
+
SET provider_metadata = jsonb_set(
|
|
104
|
+
provider_metadata,
|
|
105
|
+
'{login_code,attempts}',
|
|
106
|
+
to_jsonb((provider_metadata->'login_code'->>'attempts')::int + 1)
|
|
107
|
+
),
|
|
108
|
+
updated_at = now()
|
|
109
|
+
WHERE provider = 'email-otp'
|
|
110
|
+
AND entity_id = ?
|
|
111
|
+
AND deleted_at IS NULL
|
|
112
|
+
AND jsonb_typeof(provider_metadata->'login_code') = 'object'
|
|
113
|
+
AND (provider_metadata->'login_code'->>'attempts')::int < ?
|
|
114
|
+
AND (provider_metadata->'login_code'->>'expires_at')::timestamptz > now()
|
|
115
|
+
RETURNING provider_metadata->'login_code'->>'hash' AS hash`, [entityId, customer_auth_1.LOGIN_CODE_MAX_ATTEMPTS]);
|
|
116
|
+
const hash = result.rows[0]?.hash;
|
|
117
|
+
return typeof hash === "string" && hash ? hash : null;
|
|
118
|
+
};
|
|
119
|
+
exports.countLoginCodeAttempt = countLoginCodeAttempt;
|
|
120
|
+
/**
|
|
121
|
+
* Uses the code up: removes it (and the issuance budget, a successful login
|
|
122
|
+
* proves the address' owner holds the mailbox) if it is still the one with
|
|
123
|
+
* `hash` and not expired. Exactly one of several parallel correct logins gets
|
|
124
|
+
* true.
|
|
125
|
+
*/
|
|
126
|
+
const consumeLoginCode = async (sql, entityId, hash) => {
|
|
127
|
+
const result = await sql.raw(`UPDATE provider_identity
|
|
128
|
+
SET provider_metadata = provider_metadata - 'login_code' - 'login_code_budget',
|
|
129
|
+
updated_at = now()
|
|
130
|
+
WHERE provider = 'email-otp'
|
|
131
|
+
AND entity_id = ?
|
|
132
|
+
AND deleted_at IS NULL
|
|
133
|
+
AND provider_metadata->'login_code'->>'hash' = ?
|
|
134
|
+
AND (provider_metadata->'login_code'->>'expires_at')::timestamptz > now()
|
|
135
|
+
RETURNING id`, [entityId, hash]);
|
|
136
|
+
return result.rows.length === 1;
|
|
137
|
+
};
|
|
138
|
+
exports.consumeLoginCode = consumeLoginCode;
|
|
139
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9naW4tY29kZS1zdGF0ZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9jdXN0b21lci1hdXRoL2xvZ2luLWNvZGUtc3RhdGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsMkdBS2dGO0FBVWhGOzs7Ozs7OztHQVFHO0FBRUg7OztHQUdHO0FBQ1UsUUFBQSwrQkFBK0IsR0FBRyxDQUFDLENBQUE7QUFFaEQsMEVBQTBFO0FBQzFFLE1BQU0sOEJBQThCLEdBQUcsRUFBRSxDQUFBO0FBRXpDLGlGQUFpRjtBQUNqRixNQUFNLHdCQUF3QixHQUFHOzs7MkNBR1UsOEJBQThCOztFQUV2RSxDQUFBO0FBVUY7Ozs7O0dBS0c7QUFDSSxNQUFNLGNBQWMsR0FBRyxLQUFLLEVBQ2pDLEdBQWlCLEVBQ2pCLGtCQUEwQixFQUMxQixJQUFZLEVBQ21CLEVBQUU7SUFDakMsTUFBTSxNQUFNLEdBQUcsTUFBTSxHQUFHLENBQUMsR0FBRyxDQUMxQjs7Ozs7Ozs7Ozs7Ozs7O3NCQWVrQix3QkFBd0I7Ozs7O3NCQUt4Qix3QkFBd0I7Ozs7Ozs7Ozs7Ozs7Ozs7ZUFnQi9CLHdCQUF3Qjs7O2tCQUdyQixFQUNkO1FBQ0UsSUFBSTtRQUNKLHNDQUFzQjtRQUN0QixrQkFBa0I7UUFDbEIsa0RBQWtDLEdBQUcsdUNBQStCO1FBQ3BFLHNDQUFzQjtLQUN2QixDQUNGLENBQUE7SUFDRCxJQUFJLE1BQU0sQ0FBQyxJQUFJLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQzdCLE9BQU8sUUFBUSxDQUFBO0lBQ2pCLENBQUM7SUFDRCx5RUFBeUU7SUFDekUsK0JBQStCO0lBQy9CLE1BQU0sTUFBTSxHQUFHLE1BQU0sR0FBRyxDQUFDLEdBQUcsQ0FDMUI7Ozs7YUFJUyx3QkFBd0I7eUVBQ29DLEVBQ3JFLENBQUMsa0JBQWtCLEVBQUUsc0NBQXNCLENBQUMsQ0FDN0MsQ0FBQTtJQUNELE9BQU8sTUFBTSxDQUFDLElBQUksQ0FBQyxNQUFNLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQTtBQUMxRCxDQUFDLENBQUE7QUFyRVksUUFBQSxjQUFjLGtCQXFFMUI7QUFFRDs7Ozs7R0FLRztBQUNJLE1BQU0scUJBQXFCLEdBQUcsS0FBSyxFQUN4QyxHQUFpQixFQUNqQixRQUFnQixFQUNRLEVBQUU7SUFDMUIsTUFBTSxNQUFNLEdBQUcsTUFBTSxHQUFHLENBQUMsR0FBRyxDQUMxQjs7Ozs7Ozs7Ozs7OztnRUFhNEQsRUFDNUQsQ0FBQyxRQUFRLEVBQUUsdUNBQXVCLENBQUMsQ0FDcEMsQ0FBQTtJQUNELE1BQU0sSUFBSSxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsSUFBSSxDQUFBO0lBQ2pDLE9BQU8sT0FBTyxJQUFJLEtBQUssUUFBUSxJQUFJLElBQUksQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUE7QUFDdkQsQ0FBQyxDQUFBO0FBdkJZLFFBQUEscUJBQXFCLHlCQXVCakM7QUFFRDs7Ozs7R0FLRztBQUNJLE1BQU0sZ0JBQWdCLEdBQUcsS0FBSyxFQUNuQyxHQUFpQixFQUNqQixRQUFnQixFQUNoQixJQUFZLEVBQ00sRUFBRTtJQUNwQixNQUFNLE1BQU0sR0FBRyxNQUFNLEdBQUcsQ0FBQyxHQUFHLENBQzFCOzs7Ozs7OztrQkFRYyxFQUNkLENBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxDQUNqQixDQUFBO0lBQ0QsT0FBTyxNQUFNLENBQUMsSUFBSSxDQUFDLE1BQU0sS0FBSyxDQUFDLENBQUE7QUFDakMsQ0FBQyxDQUFBO0FBbEJZLFFBQUEsZ0JBQWdCLG9CQWtCNUIifQ==
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.customerAuthPluginFeatures = void 0;
|
|
4
|
+
const customer_auth_1 = require("@hartl-services/medusa-food-supplements-contracts/schemas/customer-auth");
|
|
5
|
+
const descriptor_1 = require("../public/descriptor");
|
|
6
|
+
const PASSWORD_RULES = "Passwort-Regeln";
|
|
7
|
+
exports.customerAuthPluginFeatures = (0, descriptor_1.definePluginFeatures)({
|
|
8
|
+
plugin_key: customer_auth_1.CUSTOMER_AUTH_PLUGIN_KEY,
|
|
9
|
+
label: "Kunden-Login",
|
|
10
|
+
description: "Welche Anmeldemethoden Kunden im Shop nutzen können. Mindestens eine Methode muss eingeschaltet bleiben, sonst kann sich niemand mehr anmelden; Admin-Benutzer melden sich unabhängig davon mit E-Mail und Passwort an. Eine Änderung wirkt innerhalb von etwa 30 Sekunden im Backend, die Storefront übernimmt sie nach ihrem Zwischenspeicher (etwa eine Minute). Die Passwort-Regeln gelten nur für Kunden, solange „E-Mail + Passwort“ eingeschaltet ist; Admin-Benutzer behalten mindestens 15 Zeichen, die bei der ersten Anmeldung eines noch nicht angelegten Admin-Benutzers (Einladung) geprüft werden.",
|
|
11
|
+
summary: "Anmeldemethoden und Passwort-Regeln für Kunden.",
|
|
12
|
+
icon: "ShieldCheck",
|
|
13
|
+
switchable: false,
|
|
14
|
+
features_heading: "Anmeldemethoden",
|
|
15
|
+
features_note: "Wirkt nach ca. 30 Sekunden im Backend, in der Storefront nach ca. 1 Minute.",
|
|
16
|
+
all_off_warning: "Keine Methode eingeschaltet: Kunden können sich nicht mehr anmelden.",
|
|
17
|
+
require_enabled_feature: true,
|
|
18
|
+
features: [
|
|
19
|
+
{
|
|
20
|
+
key: customer_auth_1.CUSTOMER_AUTH_FEATURES.emailPassword,
|
|
21
|
+
label: "E-Mail + Passwort",
|
|
22
|
+
description: "Anmeldung, Registrierung und Passwort-Zurücksetzen. Aus: Das Backend lehnt diese Wege für Kunden ab; bestehende Konten melden sich dann nur noch mit den anderen eingeschalteten Methoden an.",
|
|
23
|
+
default_enabled: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: customer_auth_1.CUSTOMER_AUTH_FEATURES.emailOtp,
|
|
27
|
+
label: "Anmeldecode per E-Mail",
|
|
28
|
+
description: "Kunden fordern einen sechsstelligen Code an, der 10 Minuten gültig ist, und melden sich damit an; für neue Adressen entsteht dabei ein Kundenkonto. Ein bestehendes Passwort-Konto derselben Adresse wird nur verknüpft, wenn die Passwort-Anmeldung aus ist oder die Adresse bestätigt wurde. Die Code-Mail geht unabhängig von den Schaltern des E-Mail-Plugins hinaus. Aus: Code-Anfrage und Code-Anmeldung sind nicht möglich.",
|
|
29
|
+
default_enabled: false,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
setting_groups: [
|
|
33
|
+
{
|
|
34
|
+
name: PASSWORD_RULES,
|
|
35
|
+
description: "Gelten nur für Kunden mit „E-Mail + Passwort“, bei Registrierung und Passwort-Zurücksetzen. Admin-Benutzer behalten mindestens 15 Zeichen.",
|
|
36
|
+
requires_feature: customer_auth_1.CUSTOMER_AUTH_FEATURES.emailPassword,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
settings: [
|
|
40
|
+
{
|
|
41
|
+
key: "password_min_length",
|
|
42
|
+
label: "Mindestlänge",
|
|
43
|
+
description: "Wie viele Zeichen ein Kundenpasswort mindestens haben muss (8 bis 128). NIST SP 800-63B Rev. 4, die US-Richtlinie für digitale Identität, empfiehlt mindestens 15 Zeichen, wenn das Passwort der einzige Faktor ist. Die Regeln gelten nur, solange „E-Mail + Passwort“ eingeschaltet ist, und nur für Kunden; Admin-Benutzer behalten mindestens 15 Zeichen. Bestehende Passwörter bleiben gültig, die Regeln greifen bei Registrierung und Passwort-Zurücksetzen. Grenze: Wer sich über den Admin-Weg registriert, kann 15 Zeichen ohne Zusammensetzungsregeln wählen, auch wenn der Shop mehr verlangt; das schwächt nur das eigene Passwort.",
|
|
44
|
+
hint: "8 bis 128 Zeichen. Empfohlen: mindestens 15 (NIST SP 800-63B).",
|
|
45
|
+
group: PASSWORD_RULES,
|
|
46
|
+
width: "half",
|
|
47
|
+
type: "number",
|
|
48
|
+
min: 8,
|
|
49
|
+
max: 128,
|
|
50
|
+
integer: true,
|
|
51
|
+
default: 15,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: "password_require_mixed_case",
|
|
55
|
+
label: "Groß- und Kleinbuchstaben verlangen",
|
|
56
|
+
description: "Mindestens ein Groß- und ein Kleinbuchstabe.",
|
|
57
|
+
hint: "NIST rät von Zusammensetzungsregeln ab: Sie führen zu vorhersehbaren Passwörtern wie „Passwort1!“. Länge schützt besser.",
|
|
58
|
+
group: PASSWORD_RULES,
|
|
59
|
+
type: "boolean",
|
|
60
|
+
default: false,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: "password_require_digit",
|
|
64
|
+
label: "Ziffer verlangen",
|
|
65
|
+
description: "Mindestens eine Ziffer.",
|
|
66
|
+
group: PASSWORD_RULES,
|
|
67
|
+
type: "boolean",
|
|
68
|
+
default: false,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "password_require_symbol",
|
|
72
|
+
label: "Sonderzeichen verlangen",
|
|
73
|
+
description: "Mindestens ein Sonderzeichen (kein Buchstabe, keine Ziffer, kein Leerzeichen).",
|
|
74
|
+
group: PASSWORD_RULES,
|
|
75
|
+
type: "boolean",
|
|
76
|
+
default: false,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2luLWZlYXR1cmVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2N1c3RvbWVyLWF1dGgvcGx1Z2luLWZlYXR1cmVzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLDJHQUdnRjtBQUVoRixxREFBMkQ7QUFFM0QsTUFBTSxjQUFjLEdBQUcsaUJBQWlCLENBQUE7QUFFM0IsUUFBQSwwQkFBMEIsR0FBRyxJQUFBLGlDQUFvQixFQUFDO0lBQzdELFVBQVUsRUFBRSx3Q0FBd0I7SUFDcEMsS0FBSyxFQUFFLGNBQWM7SUFDckIsV0FBVyxFQUNULG1sQkFBbWxCO0lBQ3JsQixPQUFPLEVBQUUsaURBQWlEO0lBQzFELElBQUksRUFBRSxhQUFhO0lBQ25CLFVBQVUsRUFBRSxLQUFLO0lBQ2pCLGdCQUFnQixFQUFFLGlCQUFpQjtJQUNuQyxhQUFhLEVBQ1gsNkVBQTZFO0lBQy9FLGVBQWUsRUFDYixzRUFBc0U7SUFDeEUsdUJBQXVCLEVBQUUsSUFBSTtJQUM3QixRQUFRLEVBQUU7UUFDUjtZQUNFLEdBQUcsRUFBRSxzQ0FBc0IsQ0FBQyxhQUFhO1lBQ3pDLEtBQUssRUFBRSxtQkFBbUI7WUFDMUIsV0FBVyxFQUNULCtMQUErTDtZQUNqTSxlQUFlLEVBQUUsSUFBSTtTQUN0QjtRQUNEO1lBQ0UsR0FBRyxFQUFFLHNDQUFzQixDQUFDLFFBQVE7WUFDcEMsS0FBSyxFQUFFLHdCQUF3QjtZQUMvQixXQUFXLEVBQ1Qsb2FBQW9hO1lBQ3RhLGVBQWUsRUFBRSxLQUFLO1NBQ3ZCO0tBQ0Y7SUFDRCxjQUFjLEVBQUU7UUFDZDtZQUNFLElBQUksRUFBRSxjQUFjO1lBQ3BCLFdBQVcsRUFDVCw0SUFBNEk7WUFDOUksZ0JBQWdCLEVBQUUsc0NBQXNCLENBQUMsYUFBYTtTQUN2RDtLQUNGO0lBQ0QsUUFBUSxFQUFFO1FBQ1I7WUFDRSxHQUFHLEVBQUUscUJBQXFCO1lBQzFCLEtBQUssRUFBRSxjQUFjO1lBQ3JCLFdBQVcsRUFDVCxrbkJBQWtuQjtZQUNwbkIsSUFBSSxFQUFFLGdFQUFnRTtZQUN0RSxLQUFLLEVBQUUsY0FBYztZQUNyQixLQUFLLEVBQUUsTUFBTTtZQUNiLElBQUksRUFBRSxRQUFRO1lBQ2QsR0FBRyxFQUFFLENBQUM7WUFDTixHQUFHLEVBQUUsR0FBRztZQUNSLE9BQU8sRUFBRSxJQUFJO1lBQ2IsT0FBTyxFQUFFLEVBQUU7U0FDWjtRQUNEO1lBQ0UsR0FBRyxFQUFFLDZCQUE2QjtZQUNsQyxLQUFLLEVBQUUscUNBQXFDO1lBQzVDLFdBQVcsRUFBRSw4Q0FBOEM7WUFDM0QsSUFBSSxFQUFFLDBIQUEwSDtZQUNoSSxLQUFLLEVBQUUsY0FBYztZQUNyQixJQUFJLEVBQUUsU0FBUztZQUNmLE9BQU8sRUFBRSxLQUFLO1NBQ2Y7UUFDRDtZQUNFLEdBQUcsRUFBRSx3QkFBd0I7WUFDN0IsS0FBSyxFQUFFLGtCQUFrQjtZQUN6QixXQUFXLEVBQUUseUJBQXlCO1lBQ3RDLEtBQUssRUFBRSxjQUFjO1lBQ3JCLElBQUksRUFBRSxTQUFTO1lBQ2YsT0FBTyxFQUFFLEtBQUs7U0FDZjtRQUNEO1lBQ0UsR0FBRyxFQUFFLHlCQUF5QjtZQUM5QixLQUFLLEVBQUUseUJBQXlCO1lBQ2hDLFdBQVcsRUFBRSxnRkFBZ0Y7WUFDN0YsS0FBSyxFQUFFLGNBQWM7WUFDckIsSUFBSSxFQUFFLFNBQVM7WUFDZixPQUFPLEVBQUUsS0FBSztTQUNmO0tBQ0Y7Q0FDRixDQUFDLENBQUEifQ==
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.removeProviderIdentity = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Deletes a provider identity for good and, when no provider identity is left
|
|
6
|
+
* on its auth identity, the auth identity too. The unique index on
|
|
7
|
+
* `(entity_id, provider)` has no `deleted_at` filter, so a soft delete would
|
|
8
|
+
* block the address for a new registration.
|
|
9
|
+
*/
|
|
10
|
+
const removeProviderIdentity = async (auth, identity) => {
|
|
11
|
+
await auth.deleteProviderIdentities([identity.providerIdentityId]);
|
|
12
|
+
const remaining = await auth.listProviderIdentities({ auth_identity_id: identity.authIdentityId }, { select: ["id"], take: 1 });
|
|
13
|
+
if (!remaining.length) {
|
|
14
|
+
await auth.deleteAuthIdentities([identity.authIdentityId]);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
exports.removeProviderIdentity = removeProviderIdentity;
|
|
18
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVtb3ZlLXByb3ZpZGVyLWlkZW50aXR5LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2N1c3RvbWVyLWF1dGgvcmVtb3ZlLXByb3ZpZGVyLWlkZW50aXR5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUVBOzs7OztHQUtHO0FBQ0ksTUFBTSxzQkFBc0IsR0FBRyxLQUFLLEVBQ3pDLElBQXdCLEVBQ3hCLFFBQTBFLEVBQzNELEVBQUU7SUFDakIsTUFBTSxJQUFJLENBQUMsd0JBQXdCLENBQUMsQ0FBQyxRQUFRLENBQUMsa0JBQWtCLENBQUMsQ0FBQyxDQUFBO0lBQ2xFLE1BQU0sU0FBUyxHQUFHLE1BQU0sSUFBSSxDQUFDLHNCQUFzQixDQUNqRCxFQUFFLGdCQUFnQixFQUFFLFFBQVEsQ0FBQyxjQUFjLEVBQUUsRUFDN0MsRUFBRSxNQUFNLEVBQUUsQ0FBQyxJQUFJLENBQUMsRUFBRSxJQUFJLEVBQUUsQ0FBQyxFQUFFLENBQzVCLENBQUE7SUFDRCxJQUFJLENBQUMsU0FBUyxDQUFDLE1BQU0sRUFBRSxDQUFDO1FBQ3RCLE1BQU0sSUFBSSxDQUFDLG9CQUFvQixDQUFDLENBQUMsUUFBUSxDQUFDLGNBQWMsQ0FBQyxDQUFDLENBQUE7SUFDNUQsQ0FBQztBQUNILENBQUMsQ0FBQTtBQVpZLFFBQUEsc0JBQXNCLDBCQVlsQyJ9
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stopCustomerAuthMethods = exports.startCustomerAuthMethods = void 0;
|
|
4
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
5
|
+
const module_access_1 = require("../public/module-access");
|
|
6
|
+
const customer_auth_methods_snapshot_1 = require("./customer-auth-methods-snapshot");
|
|
7
|
+
const plugin_features_1 = require("./plugin-features");
|
|
8
|
+
/**
|
|
9
|
+
* Hands the snapshot behind `authMethodsPerActor` its reader (the effective
|
|
10
|
+
* `customer_auth` switches) and loads it once. Called when the application
|
|
11
|
+
* has started, with the application container: the config getter that needs
|
|
12
|
+
* the switches has no container of its own.
|
|
13
|
+
*/
|
|
14
|
+
const startCustomerAuthMethods = (container) => (0, customer_auth_methods_snapshot_1.startCustomerAuthMethodsSnapshot)({
|
|
15
|
+
read: async () => (await (0, module_access_1.resolveBaseModule)(container).retrieveEffectiveFeatures(plugin_features_1.customerAuthPluginFeatures)).features,
|
|
16
|
+
warn: (message) => container.resolve(utils_1.ContainerRegistrationKeys.LOGGER).warn(message),
|
|
17
|
+
});
|
|
18
|
+
exports.startCustomerAuthMethods = startCustomerAuthMethods;
|
|
19
|
+
exports.stopCustomerAuthMethods = customer_auth_methods_snapshot_1.stopCustomerAuthMethodsSnapshot;
|
|
20
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RhcnQtY3VzdG9tZXItYXV0aC1tZXRob2RzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2N1c3RvbWVyLWF1dGgvc3RhcnQtY3VzdG9tZXItYXV0aC1tZXRob2RzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUNBLHFEQUFxRTtBQUVyRSwyREFBMkQ7QUFFM0QscUZBR3lDO0FBQ3pDLHVEQUE4RDtBQVE5RDs7Ozs7R0FLRztBQUNJLE1BQU0sd0JBQXdCLEdBQUcsQ0FDdEMsU0FBMEIsRUFDWCxFQUFFLENBQ2pCLElBQUEsaUVBQWdDLEVBQUM7SUFDL0IsSUFBSSxFQUFFLEtBQUssSUFBSSxFQUFFLENBQ2YsQ0FDRSxNQUFNLElBQUEsaUNBQWlCLEVBQ3JCLFNBQVMsQ0FDVixDQUFDLHlCQUF5QixDQUFDLDRDQUEwQixDQUFDLENBQ3hELENBQUMsUUFBUTtJQUNaLElBQUksRUFBRSxDQUFDLE9BQU8sRUFBRSxFQUFFLENBQ2hCLFNBQVMsQ0FBQyxPQUFPLENBQVMsaUNBQXlCLENBQUMsTUFBTSxDQUFDLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQztDQUM1RSxDQUFDLENBQUE7QUFaUyxRQUFBLHdCQUF3Qiw0QkFZakM7QUFFUyxRQUFBLHVCQUF1QixHQUFHLGdFQUErQixDQUFBIn0=
|
|
@@ -6,6 +6,10 @@ exports.documentsPluginFeatures = (0, public_1.definePluginFeatures)({
|
|
|
6
6
|
plugin_key: "documents",
|
|
7
7
|
label: "Dokumente",
|
|
8
8
|
description: "Rechnungen, Stornos und Packlisten zu Bestellungen. Ausgeschaltet sind alle Dokument-Routen in Admin und Storefront nicht erreichbar und es werden keine neuen Dokumente erstellt; bereits ausgestellte Dokumente bleiben gespeichert. Checkout, Fulfillment und Versand laufen unverändert weiter.",
|
|
9
|
+
summary: "Rechnungen, Stornos und Packlisten zu Bestellungen.",
|
|
10
|
+
icon: "DocumentText",
|
|
11
|
+
disable_hint: "Aus: Dokument-Routen sind nicht erreichbar, es entstehen keine neuen Dokumente. Bestehende bleiben gespeichert.",
|
|
12
|
+
disable_confirm: "Alle Dokument-Routen in Admin und Storefront sind dann nicht erreichbar, und es werden keine neuen Dokumente erstellt. Bereits ausgestellte Dokumente bleiben gespeichert. Checkout, Fulfillment und Versand laufen unverändert weiter.",
|
|
9
13
|
features: [
|
|
10
14
|
{
|
|
11
15
|
key: "invoices",
|
|
@@ -16,7 +20,7 @@ exports.documentsPluginFeatures = (0, public_1.definePluginFeatures)({
|
|
|
16
20
|
{
|
|
17
21
|
key: "packing_lists",
|
|
18
22
|
label: "Packlisten",
|
|
19
|
-
description: "
|
|
23
|
+
description: "Eine Packliste je Fulfillment, inklusive Chargenrevisionen und Abruf im Admin.",
|
|
20
24
|
default_enabled: true,
|
|
21
25
|
},
|
|
22
26
|
],
|
|
@@ -25,8 +29,10 @@ exports.documentsPluginFeatures = (0, public_1.definePluginFeatures)({
|
|
|
25
29
|
key: "payment_terms",
|
|
26
30
|
label: "Zahlungskonditionen",
|
|
27
31
|
description: "Erscheint auf Rechnungen unter den Summen, z. B. „Der Betrag wurde bei der Bestellung per Kreditkarte bezahlt.“ Leer lassen, wenn nichts stehen soll. Gilt für neu erstellte und neu erzeugte Rechnungen.",
|
|
32
|
+
hint: "Erscheint auf Rechnungen unter den Summen.",
|
|
33
|
+
placeholder: "z. B. Der Betrag wurde bei der Bestellung per Kreditkarte bezahlt.",
|
|
28
34
|
type: "string",
|
|
29
35
|
},
|
|
30
36
|
],
|
|
31
37
|
});
|
|
32
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
38
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2luLWZlYXR1cmVzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2RvY3VtZW50cy9wbHVnaW4tZmVhdHVyZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsc0NBQWdEO0FBRW5DLFFBQUEsdUJBQXVCLEdBQUcsSUFBQSw2QkFBb0IsRUFBQztJQUMxRCxVQUFVLEVBQUUsV0FBVztJQUN2QixLQUFLLEVBQUUsV0FBVztJQUNsQixXQUFXLEVBQ1QscVNBQXFTO0lBQ3ZTLE9BQU8sRUFBRSxxREFBcUQ7SUFDOUQsSUFBSSxFQUFFLGNBQWM7SUFDcEIsWUFBWSxFQUNWLGlIQUFpSDtJQUNuSCxlQUFlLEVBQ2IseU9BQXlPO0lBQzNPLFFBQVEsRUFBRTtRQUNSO1lBQ0UsR0FBRyxFQUFFLFVBQVU7WUFDZixLQUFLLEVBQUUsc0JBQXNCO1lBQzdCLFdBQVcsRUFDVCxzYkFBc2I7WUFDeGIsZUFBZSxFQUFFLElBQUk7U0FDdEI7UUFDRDtZQUNFLEdBQUcsRUFBRSxlQUFlO1lBQ3BCLEtBQUssRUFBRSxZQUFZO1lBQ25CLFdBQVcsRUFDVCxnRkFBZ0Y7WUFDbEYsZUFBZSxFQUFFLElBQUk7U0FDdEI7S0FDRjtJQUNELFFBQVEsRUFBRTtRQUNSO1lBQ0UsR0FBRyxFQUFFLGVBQWU7WUFDcEIsS0FBSyxFQUFFLHFCQUFxQjtZQUM1QixXQUFXLEVBQ1QsMk1BQTJNO1lBQzdNLElBQUksRUFBRSw0Q0FBNEM7WUFDbEQsV0FBVyxFQUFFLG9FQUFvRTtZQUNqRixJQUFJLEVBQUUsUUFBUTtTQUNmO0tBQ0Y7Q0FDRixDQUFDLENBQUEifQ==
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deliverEphemeralAuthMail = void 0;
|
|
4
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
5
|
+
const failure_classification_1 = require("../domain/failure-classification");
|
|
6
|
+
const load_email_shop_details_1 = require("../rendering/load-email-shop-details");
|
|
7
|
+
const load_template_1 = require("../templates/load-template");
|
|
8
|
+
const dispatch_email_message_1 = require("../../workflows/email/dispatch-email-message");
|
|
9
|
+
const asNotificationRecord = (value) => {
|
|
10
|
+
if (!value || typeof value !== "object") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const record = value;
|
|
14
|
+
if (typeof record.id !== "string" ||
|
|
15
|
+
!record.id.trim() ||
|
|
16
|
+
(record.status !== "failure" &&
|
|
17
|
+
record.status !== "pending" &&
|
|
18
|
+
record.status !== "success")) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return { id: record.id, status: record.status };
|
|
22
|
+
};
|
|
23
|
+
const failClaim = async (email, claim, code, now, notificationId = null) => {
|
|
24
|
+
const completed = await email.markAttemptFailed({
|
|
25
|
+
attempt_id: claim.attempt.id,
|
|
26
|
+
attempt_token: claim.attempt_token,
|
|
27
|
+
failure_code: code,
|
|
28
|
+
finished_at: now,
|
|
29
|
+
notification_id: notificationId,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
kind: "done",
|
|
33
|
+
result: {
|
|
34
|
+
attempt_id: claim.attempt.id,
|
|
35
|
+
code,
|
|
36
|
+
message_id: completed.message.id,
|
|
37
|
+
status: "failed",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* One delivery attempt of a claimed message whose content carries a secret
|
|
43
|
+
* that lives only in the caller's `render` closure (password reset link,
|
|
44
|
+
* account confirmation link, login code): renders it, renews the lease, hands
|
|
45
|
+
* the content to the notification module and records the outcome on the
|
|
46
|
+
* attempt. The message stores no template data of the secret, the notification
|
|
47
|
+
* record keeps only ids, and nothing here is logged.
|
|
48
|
+
*
|
|
49
|
+
* Customer mails only (`resource_type` "customer").
|
|
50
|
+
*/
|
|
51
|
+
const deliverEphemeralAuthMail = async ({ claim, container, email, expiresAt, getNow, kind, maxAttempts, render, retryDelayMs, triggerType, }) => {
|
|
52
|
+
// A stored template never fails the mail: it falls back to the default.
|
|
53
|
+
const [shop, template] = await Promise.all([
|
|
54
|
+
(0, load_email_shop_details_1.loadEmailShopDetails)(container),
|
|
55
|
+
(0, load_template_1.loadEmailTemplate)(container, email, kind),
|
|
56
|
+
]);
|
|
57
|
+
let rendered;
|
|
58
|
+
try {
|
|
59
|
+
rendered = render({
|
|
60
|
+
configuration: email.getDispatchConfiguration(),
|
|
61
|
+
shop,
|
|
62
|
+
template,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return await failClaim(email, claim, "render_failed", getNow());
|
|
67
|
+
}
|
|
68
|
+
const beforeNotification = getNow();
|
|
69
|
+
if (expiresAt.getTime() <= beforeNotification.getTime()) {
|
|
70
|
+
return await failClaim(email, claim, "payload_expired", beforeNotification);
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await email.renewAttemptLease({
|
|
74
|
+
attempt_id: claim.attempt.id,
|
|
75
|
+
attempt_token: claim.attempt_token,
|
|
76
|
+
now: beforeNotification,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { kind: "retry" };
|
|
81
|
+
}
|
|
82
|
+
const notifications = container.resolve(utils_1.Modules.NOTIFICATION);
|
|
83
|
+
const safeData = {
|
|
84
|
+
attempt_id: claim.attempt.id,
|
|
85
|
+
kind: claim.message.kind,
|
|
86
|
+
message_id: claim.message.id,
|
|
87
|
+
};
|
|
88
|
+
let creationError;
|
|
89
|
+
let created = null;
|
|
90
|
+
try {
|
|
91
|
+
created = asNotificationRecord(await notifications.createNotifications({
|
|
92
|
+
channel: "email",
|
|
93
|
+
content: {
|
|
94
|
+
html: rendered.html,
|
|
95
|
+
subject: rendered.subject,
|
|
96
|
+
text: rendered.text,
|
|
97
|
+
},
|
|
98
|
+
data: safeData,
|
|
99
|
+
idempotency_key: (0, failure_classification_1.emailAttemptNotificationKey)(claim.attempt.id),
|
|
100
|
+
...(claim.previous_notification_id
|
|
101
|
+
? { original_notification_id: claim.previous_notification_id }
|
|
102
|
+
: {}),
|
|
103
|
+
provider_data: safeData,
|
|
104
|
+
...(claim.message.resource_id
|
|
105
|
+
? { resource_id: claim.message.resource_id }
|
|
106
|
+
: {}),
|
|
107
|
+
resource_type: "customer",
|
|
108
|
+
template: claim.message.kind,
|
|
109
|
+
to: claim.message.recipient,
|
|
110
|
+
trigger_type: triggerType,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
creationError = error;
|
|
115
|
+
}
|
|
116
|
+
let persisted = null;
|
|
117
|
+
let lookupFailed = false;
|
|
118
|
+
if (!created || creationError) {
|
|
119
|
+
try {
|
|
120
|
+
persisted = await (0, dispatch_email_message_1.findAttemptNotification)(container, claim.attempt.id);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
lookupFailed = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const notification = created ?? persisted;
|
|
127
|
+
const finishedAt = getNow();
|
|
128
|
+
if (notification?.status === "success") {
|
|
129
|
+
const completed = await email.markAttemptSent({
|
|
130
|
+
attempt_id: claim.attempt.id,
|
|
131
|
+
attempt_token: claim.attempt_token,
|
|
132
|
+
finished_at: finishedAt,
|
|
133
|
+
notification_id: notification.id,
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
kind: "done",
|
|
137
|
+
result: {
|
|
138
|
+
attempt_id: claim.attempt.id,
|
|
139
|
+
message_id: completed.message.id,
|
|
140
|
+
status: "sent",
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const failure = creationError && notification?.status === "failure" && !lookupFailed
|
|
145
|
+
? (0, failure_classification_1.classifyNotificationFailure)(creationError)
|
|
146
|
+
: { code: "outcome_unknown", outcome: "unknown" };
|
|
147
|
+
if (failure.outcome === "retryable") {
|
|
148
|
+
const nextAttemptAt = new Date(finishedAt.getTime() + retryDelayMs);
|
|
149
|
+
if (claim.attempt.attempt_number < maxAttempts &&
|
|
150
|
+
nextAttemptAt.getTime() < expiresAt.getTime()) {
|
|
151
|
+
await email.markAttemptRetryable({
|
|
152
|
+
attempt_id: claim.attempt.id,
|
|
153
|
+
attempt_token: claim.attempt_token,
|
|
154
|
+
failure_code: failure.code,
|
|
155
|
+
finished_at: finishedAt,
|
|
156
|
+
next_attempt_at: nextAttemptAt,
|
|
157
|
+
notification_id: notification?.id ?? null,
|
|
158
|
+
});
|
|
159
|
+
return { kind: "retry" };
|
|
160
|
+
}
|
|
161
|
+
return await failClaim(email, claim, expiresAt.getTime() <= nextAttemptAt.getTime()
|
|
162
|
+
? "payload_expired"
|
|
163
|
+
: "retry_exhausted", finishedAt, notification?.id ?? null);
|
|
164
|
+
}
|
|
165
|
+
if (failure.outcome === "failed") {
|
|
166
|
+
return await failClaim(email, claim, failure.code, finishedAt, notification?.id ?? null);
|
|
167
|
+
}
|
|
168
|
+
const completed = await email.markAttemptUnknown({
|
|
169
|
+
attempt_id: claim.attempt.id,
|
|
170
|
+
attempt_token: claim.attempt_token,
|
|
171
|
+
failure_code: failure.code,
|
|
172
|
+
finished_at: finishedAt,
|
|
173
|
+
notification_id: notification?.id ?? null,
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
kind: "done",
|
|
177
|
+
result: {
|
|
178
|
+
attempt_id: claim.attempt.id,
|
|
179
|
+
code: failure.code,
|
|
180
|
+
message_id: completed.message.id,
|
|
181
|
+
status: "unknown",
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
exports.deliverEphemeralAuthMail = deliverEphemeralAuthMail;
|
|
186
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVsaXZlci1lcGhlbWVyYWwtYXV0aC1tYWlsLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vc3JjL2VtYWlsL2FwcGxpY2F0aW9uL2RlbGl2ZXItZXBoZW1lcmFsLWF1dGgtbWFpbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFJQSxxREFBb0Q7QUFFcEQsNkVBRzBDO0FBRzFDLGtGQUE0RTtBQUU1RSw4REFBK0Q7QUFPL0QseUZBQXVGO0FBK0N2RixNQUFNLG9CQUFvQixHQUFHLENBQUMsS0FBYyxFQUE2QixFQUFFO0lBQ3pFLElBQUksQ0FBQyxLQUFLLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxFQUFFLENBQUM7UUFDeEMsT0FBTyxJQUFJLENBQUM7SUFDZCxDQUFDO0lBQ0QsTUFBTSxNQUFNLEdBQUcsS0FBZ0MsQ0FBQztJQUNoRCxJQUNFLE9BQU8sTUFBTSxDQUFDLEVBQUUsS0FBSyxRQUFRO1FBQzdCLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxJQUFJLEVBQUU7UUFDakIsQ0FBQyxNQUFNLENBQUMsTUFBTSxLQUFLLFNBQVM7WUFDMUIsTUFBTSxDQUFDLE1BQU0sS0FBSyxTQUFTO1lBQzNCLE1BQU0sQ0FBQyxNQUFNLEtBQUssU0FBUyxDQUFDLEVBQzlCLENBQUM7UUFDRCxPQUFPLElBQUksQ0FBQztJQUNkLENBQUM7SUFDRCxPQUFPLEVBQUUsRUFBRSxFQUFFLE1BQU0sQ0FBQyxFQUFFLEVBQUUsTUFBTSxFQUFFLE1BQU0sQ0FBQyxNQUFNLEVBQUUsQ0FBQztBQUNsRCxDQUFDLENBQUM7QUFFRixNQUFNLFNBQVMsR0FBRyxLQUFLLEVBQ3JCLEtBQXlCLEVBQ3pCLEtBQXdCLEVBQ3hCLElBQVksRUFDWixHQUFTLEVBQ1QsaUJBQWdDLElBQUksRUFDRCxFQUFFO0lBQ3JDLE1BQU0sU0FBUyxHQUFHLE1BQU0sS0FBSyxDQUFDLGlCQUFpQixDQUFDO1FBQzlDLFVBQVUsRUFBRSxLQUFLLENBQUMsT0FB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
|