@reactionary/magento 0.12.4 → 0.12.7
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.
|
@@ -9,20 +9,21 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
9
9
|
return result;
|
|
10
10
|
};
|
|
11
11
|
import {
|
|
12
|
-
IdentityCapability,
|
|
13
12
|
Reactionary,
|
|
14
13
|
IdentityQuerySelfSchema,
|
|
15
14
|
IdentitySchema,
|
|
16
15
|
IdentityMutationRegisterSchema,
|
|
17
16
|
IdentityMutationLoginSchema,
|
|
18
17
|
IdentityMutationLogoutSchema,
|
|
18
|
+
error,
|
|
19
19
|
success
|
|
20
20
|
} from "@reactionary/core";
|
|
21
|
+
import { OidcIdentityCapability } from "@reactionary/oidc";
|
|
21
22
|
import createDebug from "debug";
|
|
22
23
|
const debug = createDebug("reactionary:magento:identity");
|
|
23
|
-
class MagentoIdentityCapability extends
|
|
24
|
+
class MagentoIdentityCapability extends OidcIdentityCapability {
|
|
24
25
|
constructor(config, cache, context, magentoApi) {
|
|
25
|
-
super(cache, context);
|
|
26
|
+
super(cache, context, config.externalIdentityProviders);
|
|
26
27
|
this.magentoApi = magentoApi;
|
|
27
28
|
this.config = config;
|
|
28
29
|
}
|
|
@@ -92,6 +93,21 @@ class MagentoIdentityCapability extends IdentityCapability {
|
|
|
92
93
|
password: payload.password
|
|
93
94
|
});
|
|
94
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Stock Magento has no external login. Once the shared OpenID Connect client has verified the identity, this
|
|
98
|
+
* finds or creates the customer by verified email (the pattern in Adobe's social login guide) and issues a
|
|
99
|
+
* customer token. An existing account with the same email is signed in, which is only appropriate for identity
|
|
100
|
+
* providers the shop owner trusts: that is what the configured provider list is.
|
|
101
|
+
*/
|
|
102
|
+
async exchangeExternalIdentity(input) {
|
|
103
|
+
const email = input.claims["email"];
|
|
104
|
+
if (typeof email !== "string" || email.length === 0) {
|
|
105
|
+
return error({ type: "InvalidInput", error: "The identity provider did not return an email address" });
|
|
106
|
+
}
|
|
107
|
+
await this.magentoApi.loginWithExternalIdentity({ email, ...namesFromClaims(input.claims) });
|
|
108
|
+
const me = await this.magentoApi.getMe();
|
|
109
|
+
return success({ type: "Registered", id: { userId: String(me.id) } });
|
|
110
|
+
}
|
|
95
111
|
}
|
|
96
112
|
__decorateClass([
|
|
97
113
|
Reactionary({
|
|
@@ -117,6 +133,16 @@ __decorateClass([
|
|
|
117
133
|
outputSchema: IdentitySchema
|
|
118
134
|
})
|
|
119
135
|
], MagentoIdentityCapability.prototype, "register", 1);
|
|
136
|
+
function namesFromClaims(claims) {
|
|
137
|
+
const text = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
138
|
+
const given = text(claims["given_name"]);
|
|
139
|
+
const family = text(claims["family_name"]);
|
|
140
|
+
if (given || family) {
|
|
141
|
+
return { firstName: given ?? "User", lastName: family ?? "Account" };
|
|
142
|
+
}
|
|
143
|
+
const [first, ...rest] = (text(claims["name"]) ?? "").split(" ").filter(Boolean);
|
|
144
|
+
return { firstName: first ?? "User", lastName: rest.join(" ") || "Account" };
|
|
145
|
+
}
|
|
120
146
|
export {
|
|
121
147
|
MagentoIdentityCapability
|
|
122
148
|
};
|
package/core/client.js
CHANGED
|
@@ -15,6 +15,11 @@ function ensureMagentoPasswordPolicy(password) {
|
|
|
15
15
|
if (!/[^A-Za-z0-9]/.test(password)) suffix += "!";
|
|
16
16
|
return `${password}${suffix}`;
|
|
17
17
|
}
|
|
18
|
+
function randomMagentoPassword() {
|
|
19
|
+
const bytes = new Uint8Array(24);
|
|
20
|
+
crypto.getRandomValues(bytes);
|
|
21
|
+
return ensureMagentoPasswordPolicy(btoa(String.fromCharCode(...bytes)));
|
|
22
|
+
}
|
|
18
23
|
class RequestContextTokenStore {
|
|
19
24
|
constructor(context, keyPrefix = "__x") {
|
|
20
25
|
this.context = context;
|
|
@@ -160,6 +165,34 @@ class Magento {
|
|
|
160
165
|
await this.tokenStore.setItem("customerToken", normalized);
|
|
161
166
|
return normalized;
|
|
162
167
|
},
|
|
168
|
+
/**
|
|
169
|
+
* Establishes a customer session for a verified external identity, the way Adobe's social login guide does:
|
|
170
|
+
* find the customer by email, create it if there is none, then issue a customer token with the admin key.
|
|
171
|
+
* Needs the Storefront Compatibility Package (4.7.6+) for POST /V1/customers/{id}/token.
|
|
172
|
+
*/
|
|
173
|
+
loginWithExternalIdentity: async (identity) => {
|
|
174
|
+
const params = new URLSearchParams();
|
|
175
|
+
params.set("searchCriteria[filterGroups][0][filters][0][field]", "email");
|
|
176
|
+
params.set("searchCriteria[filterGroups][0][filters][0][value]", identity.email);
|
|
177
|
+
params.set("searchCriteria[filterGroups][0][filters][0][condition_type]", "eq");
|
|
178
|
+
params.set("searchCriteria[pageSize]", "1");
|
|
179
|
+
const found = await this.adminRest.request(
|
|
180
|
+
"GET",
|
|
181
|
+
`/V1/customers/search?${params.toString()}`
|
|
182
|
+
);
|
|
183
|
+
let customerId = found.items?.[0]?.id;
|
|
184
|
+
if (customerId === void 0) {
|
|
185
|
+
const created = await this.adminRest.request("POST", "/V1/customers", {
|
|
186
|
+
customer: { email: identity.email, firstname: identity.firstName, lastname: identity.lastName },
|
|
187
|
+
password: randomMagentoPassword()
|
|
188
|
+
});
|
|
189
|
+
customerId = created.id;
|
|
190
|
+
}
|
|
191
|
+
const token = await this.adminRest.request("POST", `/V1/customers/${customerId}/token`);
|
|
192
|
+
const normalized = typeof token === "string" ? token.replace(/^"|"$/g, "") : String(token);
|
|
193
|
+
await this.tokenStore.setItem("customerToken", normalized);
|
|
194
|
+
return normalized;
|
|
195
|
+
},
|
|
163
196
|
logout: async () => {
|
|
164
197
|
await this.tokenStore.removeItem("customerToken");
|
|
165
198
|
}
|
|
@@ -297,6 +330,16 @@ class Magento {
|
|
|
297
330
|
}
|
|
298
331
|
},
|
|
299
332
|
cart: {
|
|
333
|
+
/**
|
|
334
|
+
* Moves a guest cart's items into the signed-in customer's cart (Adobe's recommended step after login).
|
|
335
|
+
* Must run with the customer token already stored.
|
|
336
|
+
*/
|
|
337
|
+
mergeGuestCart: async (guestCartId) => {
|
|
338
|
+
await this.gql.request(
|
|
339
|
+
"mutation MergeGuestCart($source: String!) { mergeCarts(source_cart_id: $source) { id } }",
|
|
340
|
+
{ source: guestCartId }
|
|
341
|
+
);
|
|
342
|
+
},
|
|
300
343
|
create: async (customerToken) => {
|
|
301
344
|
if (customerToken) {
|
|
302
345
|
const res2 = await this.rest.request("POST", "/V1/carts/mine");
|
|
@@ -455,6 +498,25 @@ class MagentoClient {
|
|
|
455
498
|
const client = await this.getClient();
|
|
456
499
|
return client.auth.login(email, password);
|
|
457
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Signs in a verified external identity (see Magento.auth.loginWithExternalIdentity) and carries a guest cart over.
|
|
503
|
+
* The guest cart id is cleared either way: after login the cart endpoints use /carts/mine, and a stale id would
|
|
504
|
+
* make getSelf report a guest after the next logout.
|
|
505
|
+
*/
|
|
506
|
+
async loginWithExternalIdentity(identity) {
|
|
507
|
+
const client = await this.getClient();
|
|
508
|
+
const guestCartId = await this.getActiveCartId();
|
|
509
|
+
const token = await client.auth.loginWithExternalIdentity(identity);
|
|
510
|
+
if (guestCartId) {
|
|
511
|
+
try {
|
|
512
|
+
await client.store.cart.mergeGuestCart(guestCartId);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
debug("Could not merge the guest cart after external login:", err);
|
|
515
|
+
}
|
|
516
|
+
await this.clearActiveCartId();
|
|
517
|
+
}
|
|
518
|
+
return token;
|
|
519
|
+
}
|
|
458
520
|
async logout() {
|
|
459
521
|
const client = await this.getClient();
|
|
460
522
|
return client.auth.logout();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactionary/magento",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
"url": "git+https://github.com/Solteq/reactionary.git"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@reactionary/core": "0.12.
|
|
12
|
+
"@reactionary/core": "0.12.7",
|
|
13
13
|
"zod": "4.1.9",
|
|
14
|
-
"debug": "^4.4.3"
|
|
14
|
+
"debug": "^4.4.3",
|
|
15
|
+
"@reactionary/oidc": "0.12.7"
|
|
15
16
|
},
|
|
16
17
|
"devDependencies": {
|
|
17
18
|
"vitest": "^4.0.9"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
+
import { ExternalIdentityProviderSchema } from "@reactionary/oidc";
|
|
2
3
|
const MagentoConfigurationSchema = z.looseObject({
|
|
3
4
|
adminApiKey: z.string().meta({ description: "The API key for Magento admin operations. Needed for the few tasks that require admin access." }),
|
|
4
5
|
baseUrl: z.string().meta({ description: "The base URL for the Magento installation." }),
|
|
@@ -11,6 +12,7 @@ const MagentoConfigurationSchema = z.looseObject({
|
|
|
11
12
|
reviewRatingCode: z.string().optional().meta({ description: 'The name of the Magento review rating (as listed by `productReviewRatingsMetadata`) that carries the overall star rating. Defaults to "Rating"; the first available rating is used as a fallback.' }),
|
|
12
13
|
storeCode: z.string().default(""),
|
|
13
14
|
checkoutAgreementIds: z.array(z.string()).optional().meta({ description: "Ids of the Magento terms and conditions (Magento_CheckoutAgreements) the shopper accepts when placing an order. Required by stores with \"Enable Terms and Conditions\" on, which otherwise reject order placement. Sent as the payment method's `extension_attributes.agreement_ids`. The storefront must collect the shopper's consent before finalizing checkout." }),
|
|
15
|
+
externalIdentityProviders: z.array(ExternalIdentityProviderSchema).optional().meta({ description: "OpenID Connect identity providers shoppers can sign in with. Stock Magento has no social login, so reactionary runs the OpenID Connect flow and then creates or finds the customer by verified email and issues a customer token (Adobe's documented pattern). The admin API key needs the Magento_Customer::retrieve_tokens permission and the Storefront Compatibility Package (4.7.6 or newer) must be installed." }),
|
|
14
16
|
authStoreCode: z.string().default("default").meta({ description: "The store/website scope used for customer authentication and account operations (token, customers/me). Customers typically live on the default website, which may differ from the catalog storeCode." })
|
|
15
17
|
});
|
|
16
18
|
export {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type Identity, type IdentityMutationLogin, type IdentityMutationLogout, type IdentityMutationRegister, type IdentityQuerySelf, type RequestContext, type Cache,
|
|
1
|
+
import { type Identity, type IdentityConflictError, type IdentityMutationLogin, type IdentityMutationLogout, type IdentityMutationRegister, type IdentityQuerySelf, type RequestContext, type Cache, type AnonymousIdentity, type Result } from '@reactionary/core';
|
|
2
|
+
import { OidcIdentityCapability, type ExchangeExternalIdentityInput } from '@reactionary/oidc';
|
|
2
3
|
import type { MagentoClient } from '../core/client.js';
|
|
3
4
|
import type { MagentoConfiguration } from '../schema/configuration.schema.js';
|
|
4
|
-
export declare class MagentoIdentityCapability extends
|
|
5
|
+
export declare class MagentoIdentityCapability extends OidcIdentityCapability {
|
|
5
6
|
magentoApi: MagentoClient;
|
|
6
7
|
protected config: MagentoConfiguration;
|
|
7
8
|
constructor(config: MagentoConfiguration, cache: Cache, context: RequestContext, magentoApi: MagentoClient);
|
|
@@ -10,4 +11,11 @@ export declare class MagentoIdentityCapability extends IdentityCapability {
|
|
|
10
11
|
login(payload: IdentityMutationLogin): Promise<Result<Identity>>;
|
|
11
12
|
logout(_payload: IdentityMutationLogout): Promise<Result<Identity>>;
|
|
12
13
|
register(payload: IdentityMutationRegister): Promise<Result<Identity>>;
|
|
14
|
+
/**
|
|
15
|
+
* Stock Magento has no external login. Once the shared OpenID Connect client has verified the identity, this
|
|
16
|
+
* finds or creates the customer by verified email (the pattern in Adobe's social login guide) and issues a
|
|
17
|
+
* customer token. An existing account with the same email is signed in, which is only appropriate for identity
|
|
18
|
+
* providers the shop owner trusts: that is what the configured provider list is.
|
|
19
|
+
*/
|
|
20
|
+
protected exchangeExternalIdentity(input: ExchangeExternalIdentityInput): Promise<Result<Identity, IdentityConflictError>>;
|
|
13
21
|
}
|
package/src/core/client.d.ts
CHANGED
|
@@ -55,6 +55,16 @@ export declare class Magento {
|
|
|
55
55
|
private get gql();
|
|
56
56
|
auth: {
|
|
57
57
|
login: (email: string, password: string) => Promise<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Establishes a customer session for a verified external identity, the way Adobe's social login guide does:
|
|
60
|
+
* find the customer by email, create it if there is none, then issue a customer token with the admin key.
|
|
61
|
+
* Needs the Storefront Compatibility Package (4.7.6+) for POST /V1/customers/{id}/token.
|
|
62
|
+
*/
|
|
63
|
+
loginWithExternalIdentity: (identity: {
|
|
64
|
+
email: string;
|
|
65
|
+
firstName: string;
|
|
66
|
+
lastName: string;
|
|
67
|
+
}) => Promise<string>;
|
|
58
68
|
logout: () => Promise<void>;
|
|
59
69
|
};
|
|
60
70
|
store: {
|
|
@@ -95,6 +105,11 @@ export declare class Magento {
|
|
|
95
105
|
placeOrder: (cartId: string | null, payload: MagentoPlaceOrderPayload, customerToken?: string | null) => Promise<number>;
|
|
96
106
|
};
|
|
97
107
|
cart: {
|
|
108
|
+
/**
|
|
109
|
+
* Moves a guest cart's items into the signed-in customer's cart (Adobe's recommended step after login).
|
|
110
|
+
* Must run with the customer token already stored.
|
|
111
|
+
*/
|
|
112
|
+
mergeGuestCart: (guestCartId: string) => Promise<void>;
|
|
98
113
|
create: (customerToken?: string | null) => Promise<string>;
|
|
99
114
|
get: (cartId?: string | null, customerToken?: string | null) => Promise<any>;
|
|
100
115
|
getTotals: (cartId?: string | null, customerToken?: string | null) => Promise<any>;
|
|
@@ -128,6 +143,16 @@ export declare class MagentoClient {
|
|
|
128
143
|
constructor(config: MagentoConfiguration, context: RequestContext);
|
|
129
144
|
getClient(): Promise<Magento>;
|
|
130
145
|
login(email: string, password: string): Promise<string>;
|
|
146
|
+
/**
|
|
147
|
+
* Signs in a verified external identity (see Magento.auth.loginWithExternalIdentity) and carries a guest cart over.
|
|
148
|
+
* The guest cart id is cleared either way: after login the cart endpoints use /carts/mine, and a stale id would
|
|
149
|
+
* make getSelf report a guest after the next logout.
|
|
150
|
+
*/
|
|
151
|
+
loginWithExternalIdentity(identity: {
|
|
152
|
+
email: string;
|
|
153
|
+
firstName: string;
|
|
154
|
+
lastName: string;
|
|
155
|
+
}): Promise<string>;
|
|
131
156
|
logout(): Promise<void>;
|
|
132
157
|
getMe(): Promise<any>;
|
|
133
158
|
getCustomerToken(): Promise<string | null>;
|
|
@@ -14,6 +14,20 @@ export declare const MagentoConfigurationSchema: z.ZodObject<{
|
|
|
14
14
|
reviewRatingCode: z.ZodOptional<z.ZodString>;
|
|
15
15
|
storeCode: z.ZodDefault<z.ZodString>;
|
|
16
16
|
checkoutAgreementIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
17
|
+
externalIdentityProviders: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
18
|
+
key: z.ZodString;
|
|
19
|
+
displayName: z.ZodString;
|
|
20
|
+
brand: z.ZodOptional<z.ZodString>;
|
|
21
|
+
issuer: z.ZodURL;
|
|
22
|
+
clientId: z.ZodString;
|
|
23
|
+
clientSecret: z.ZodOptional<z.ZodString>;
|
|
24
|
+
scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
25
|
+
authorizationParams: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
26
|
+
requireVerifiedEmail: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
authorizationEndpoint: z.ZodOptional<z.ZodURL>;
|
|
28
|
+
tokenEndpoint: z.ZodOptional<z.ZodURL>;
|
|
29
|
+
jwksUri: z.ZodOptional<z.ZodURL>;
|
|
30
|
+
}, z.core.$loose>>>;
|
|
17
31
|
authStoreCode: z.ZodDefault<z.ZodString>;
|
|
18
32
|
}, z.core.$loose>;
|
|
19
33
|
export type MagentoConfiguration = z.infer<typeof MagentoConfigurationSchema>;
|