Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Cognitive-Load Audit — Credential setup screen

> **Status: Fixed — 2026-06-20T00:00:00Z**
> All 4 priority recommendations implemented: notice relabels Client ID/Secret to API Key/Secret Key (#1), notice flags project separation and separate-keys requirement (#2), `#credentials` README anchor verified with full walkthrough (#3), Environment description tightened (#4).
>
> Run 2026-06-19. Target = the n8n credential config for `FedexTrackOAuth2Api` /
> `FedexShippingOAuth2Api`. This is the first-run gate: a user who can't configure a
> credential never reaches a single operation.
Expand Down
12 changes: 9 additions & 3 deletions docs/audits/cognitive-load-operation-ui-20260619-191805-1f35.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
# Cognitive-Load / Conversion Audit — FedEx node configuration UI

> **Implementation status (2026-06-19):** Recommendations #1, #2, #3 are **shipped**.
> **Status: Fixed — 2026-06-20T00:00:00Z**
> All priority recommendations (#1–#5) plus the "worth adding" Create cost notice are **shipped**.
> - #1 — optional params (company, email, residential flag, pickup/packaging/label-stock,
> parcel dimensions) collapsed into an "Additional Fields" `collection` on Create + Get
> Rates. Create's flat panel drops from ~31 fields to ~14 + the collection.
> - #2 — Create `Service Type` defaults to `FEDEX_GROUND` (no more blank-but-required).
> - #3 — Phone Number description now states FedEx requires it.
> - #4 — `${role} Country` is now an ISO 3166-1 dropdown (`COUNTRY_OPTIONS`, default US);
> the value stays the two-letter code, so request bodies are unchanged.
> - #5 — Label Stock Type now has a description; the dimensions all-or-nothing rule is
> repeated on Length, Width, and Height (was only on Length).
> - "Worth adding" (What NOT to touch #2) — Create now carries an inline `notice` that it
> books a real shipment and bills the account (free in Sandbox, charged in Production).
>
> Locked by characterization tests (`nodes/Fedex/resources/shipping/getRates.presend.test.mts`,
> `create.presend.test.mts`) proving the emitted FedEx request bodies are byte-identical to
> pre-refactor. `pnpm test` (16/16) / `build` / `lint` all green. Open: #4 (Country dropdown),
> #5 (description normalization).
> pre-refactor. `pnpm test` (16/16) / `build` / `lint` all green.

> Source: `/cognitive-load-conversion` skill, run 2026-06-19. Target = the n8n node
> parameter panel as defined by the property builders in `nodes/Fedex/`. The property
Expand Down
21 changes: 21 additions & 0 deletions nodes/Fedex/Fedex.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,27 @@ export class Fedex implements INodeType {
],
default: 'tracking',
},
// Credential-fit guidance. n8n type-filters the credential dropdown, so a Track
// credential cannot attach to a Shipping operation (or vice versa) — the slot just
// stays red with no explanation of why the credential "won't take". These per-resource
// notices name the exact credential type each resource needs and point at the
// separate-keys-per-project trap (ADR-0004), so the red indicator becomes self-explaining.
{
displayName:
'This resource uses a <b>FedEx Track OAuth2 API</b> credential. If the credential field above is empty or shows a red mark, create or select one of that type — Track and Shipping require separate FedEx project keys, so a Shipping credential cannot be used here.',
name: 'trackingCredentialNotice',
type: 'notice',
default: '',
displayOptions: { show: { resource: ['tracking'] } },
},
{
displayName:
'These operations use a <b>FedEx Shipping OAuth2 API</b> credential. If the credential field above is empty or shows a red mark, create or select one of that type — Shipping and Track require separate FedEx project keys, so a Track credential cannot be used here.',
name: 'shippingCredentialNotice',
type: 'notice',
default: '',
displayOptions: { show: { resource: ['shipping'] } },
},
...trackingDescription,
...shippingDescription,
// Hidden auth discriminator the declarative routing engine reads to pick a credential.
Expand Down
257 changes: 257 additions & 0 deletions nodes/Fedex/countries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import type { INodePropertyOptions } from 'n8n-workflow';

// ISO 3166-1 alpha-2 country codes, names from CLDR (en). The dropdown value is the same
// two-letter code the free-text field previously accepted, so the emitted FedEx request body
// is byte-identical — this is a presentational swap that removes ISO-code recall (audit #4,
// docs/audits/cognitive-load-operation-ui-20260619-191805-1f35.md). Sorted by display name.
export const COUNTRY_OPTIONS: INodePropertyOptions[] = [
{ name: 'Afghanistan', value: 'AF' },
{ name: 'Åland Islands', value: 'AX' },
{ name: 'Albania', value: 'AL' },
{ name: 'Algeria', value: 'DZ' },
{ name: 'American Samoa', value: 'AS' },
{ name: 'Andorra', value: 'AD' },
{ name: 'Angola', value: 'AO' },
{ name: 'Anguilla', value: 'AI' },
{ name: 'Antarctica', value: 'AQ' },
{ name: 'Antigua & Barbuda', value: 'AG' },
{ name: 'Argentina', value: 'AR' },
{ name: 'Armenia', value: 'AM' },
{ name: 'Aruba', value: 'AW' },
{ name: 'Australia', value: 'AU' },
{ name: 'Austria', value: 'AT' },
{ name: 'Azerbaijan', value: 'AZ' },
{ name: 'Bahamas', value: 'BS' },
{ name: 'Bahrain', value: 'BH' },
{ name: 'Bangladesh', value: 'BD' },
{ name: 'Barbados', value: 'BB' },
{ name: 'Belarus', value: 'BY' },
{ name: 'Belgium', value: 'BE' },
{ name: 'Belize', value: 'BZ' },
{ name: 'Benin', value: 'BJ' },
{ name: 'Bermuda', value: 'BM' },
{ name: 'Bhutan', value: 'BT' },
{ name: 'Bolivia', value: 'BO' },
{ name: 'Bosnia & Herzegovina', value: 'BA' },
{ name: 'Botswana', value: 'BW' },
{ name: 'Bouvet Island', value: 'BV' },
{ name: 'Brazil', value: 'BR' },
{ name: 'British Indian Ocean Territory', value: 'IO' },
{ name: 'British Virgin Islands', value: 'VG' },
{ name: 'Brunei', value: 'BN' },
{ name: 'Bulgaria', value: 'BG' },
{ name: 'Burkina Faso', value: 'BF' },
{ name: 'Burundi', value: 'BI' },
{ name: 'Cambodia', value: 'KH' },
{ name: 'Cameroon', value: 'CM' },
{ name: 'Canada', value: 'CA' },
{ name: 'Cape Verde', value: 'CV' },
{ name: 'Caribbean Netherlands', value: 'BQ' },
{ name: 'Cayman Islands', value: 'KY' },
{ name: 'Central African Republic', value: 'CF' },
{ name: 'Chad', value: 'TD' },
{ name: 'Chile', value: 'CL' },
{ name: 'China', value: 'CN' },
{ name: 'Christmas Island', value: 'CX' },
{ name: 'Cocos (Keeling) Islands', value: 'CC' },
{ name: 'Colombia', value: 'CO' },
{ name: 'Comoros', value: 'KM' },
{ name: 'Congo - Brazzaville', value: 'CG' },
{ name: 'Congo - Kinshasa', value: 'CD' },
{ name: 'Cook Islands', value: 'CK' },
{ name: 'Costa Rica', value: 'CR' },
{ name: 'Côte d’Ivoire', value: 'CI' },
{ name: 'Croatia', value: 'HR' },
{ name: 'Cuba', value: 'CU' },
{ name: 'Curaçao', value: 'CW' },
{ name: 'Cyprus', value: 'CY' },
{ name: 'Czechia', value: 'CZ' },
{ name: 'Denmark', value: 'DK' },
{ name: 'Djibouti', value: 'DJ' },
{ name: 'Dominica', value: 'DM' },
{ name: 'Dominican Republic', value: 'DO' },
{ name: 'Ecuador', value: 'EC' },
{ name: 'Egypt', value: 'EG' },
{ name: 'El Salvador', value: 'SV' },
{ name: 'Equatorial Guinea', value: 'GQ' },
{ name: 'Eritrea', value: 'ER' },
{ name: 'Estonia', value: 'EE' },
{ name: 'Eswatini', value: 'SZ' },
{ name: 'Ethiopia', value: 'ET' },
{ name: 'Falkland Islands', value: 'FK' },
{ name: 'Faroe Islands', value: 'FO' },
{ name: 'Fiji', value: 'FJ' },
{ name: 'Finland', value: 'FI' },
{ name: 'France', value: 'FR' },
{ name: 'French Guiana', value: 'GF' },
{ name: 'French Polynesia', value: 'PF' },
{ name: 'French Southern Territories', value: 'TF' },
{ name: 'Gabon', value: 'GA' },
{ name: 'Gambia', value: 'GM' },
{ name: 'Georgia', value: 'GE' },
{ name: 'Germany', value: 'DE' },
{ name: 'Ghana', value: 'GH' },
{ name: 'Gibraltar', value: 'GI' },
{ name: 'Greece', value: 'GR' },
{ name: 'Greenland', value: 'GL' },
{ name: 'Grenada', value: 'GD' },
{ name: 'Guadeloupe', value: 'GP' },
{ name: 'Guam', value: 'GU' },
{ name: 'Guatemala', value: 'GT' },
{ name: 'Guernsey', value: 'GG' },
{ name: 'Guinea', value: 'GN' },
{ name: 'Guinea-Bissau', value: 'GW' },
{ name: 'Guyana', value: 'GY' },
{ name: 'Haiti', value: 'HT' },
{ name: 'Heard & McDonald Islands', value: 'HM' },
{ name: 'Honduras', value: 'HN' },
{ name: 'Hong Kong SAR China', value: 'HK' },
{ name: 'Hungary', value: 'HU' },
{ name: 'Iceland', value: 'IS' },
{ name: 'India', value: 'IN' },
{ name: 'Indonesia', value: 'ID' },
{ name: 'Iran', value: 'IR' },
{ name: 'Iraq', value: 'IQ' },
{ name: 'Ireland', value: 'IE' },
{ name: 'Isle of Man', value: 'IM' },
{ name: 'Israel', value: 'IL' },
{ name: 'Italy', value: 'IT' },
{ name: 'Jamaica', value: 'JM' },
{ name: 'Japan', value: 'JP' },
{ name: 'Jersey', value: 'JE' },
{ name: 'Jordan', value: 'JO' },
{ name: 'Kazakhstan', value: 'KZ' },
{ name: 'Kenya', value: 'KE' },
{ name: 'Kiribati', value: 'KI' },
{ name: 'Kuwait', value: 'KW' },
{ name: 'Kyrgyzstan', value: 'KG' },
{ name: 'Laos', value: 'LA' },
{ name: 'Latvia', value: 'LV' },
{ name: 'Lebanon', value: 'LB' },
{ name: 'Lesotho', value: 'LS' },
{ name: 'Liberia', value: 'LR' },
{ name: 'Libya', value: 'LY' },
{ name: 'Liechtenstein', value: 'LI' },
{ name: 'Lithuania', value: 'LT' },
{ name: 'Luxembourg', value: 'LU' },
{ name: 'Macao SAR China', value: 'MO' },
{ name: 'Madagascar', value: 'MG' },
{ name: 'Malawi', value: 'MW' },
{ name: 'Malaysia', value: 'MY' },
{ name: 'Maldives', value: 'MV' },
{ name: 'Mali', value: 'ML' },
{ name: 'Malta', value: 'MT' },
{ name: 'Marshall Islands', value: 'MH' },
{ name: 'Martinique', value: 'MQ' },
{ name: 'Mauritania', value: 'MR' },
{ name: 'Mauritius', value: 'MU' },
{ name: 'Mayotte', value: 'YT' },
{ name: 'Mexico', value: 'MX' },
{ name: 'Micronesia', value: 'FM' },
{ name: 'Moldova', value: 'MD' },
{ name: 'Monaco', value: 'MC' },
{ name: 'Mongolia', value: 'MN' },
{ name: 'Montenegro', value: 'ME' },
{ name: 'Montserrat', value: 'MS' },
{ name: 'Morocco', value: 'MA' },
{ name: 'Mozambique', value: 'MZ' },
{ name: 'Myanmar (Burma)', value: 'MM' },
{ name: 'Namibia', value: 'NA' },
{ name: 'Nauru', value: 'NR' },
{ name: 'Nepal', value: 'NP' },
{ name: 'Netherlands', value: 'NL' },
{ name: 'New Caledonia', value: 'NC' },
{ name: 'New Zealand', value: 'NZ' },
{ name: 'Nicaragua', value: 'NI' },
{ name: 'Niger', value: 'NE' },
{ name: 'Nigeria', value: 'NG' },
{ name: 'Niue', value: 'NU' },
{ name: 'Norfolk Island', value: 'NF' },
{ name: 'North Korea', value: 'KP' },
{ name: 'North Macedonia', value: 'MK' },
{ name: 'Northern Mariana Islands', value: 'MP' },
{ name: 'Norway', value: 'NO' },
{ name: 'Oman', value: 'OM' },
{ name: 'Pakistan', value: 'PK' },
{ name: 'Palau', value: 'PW' },
{ name: 'Palestinian Territories', value: 'PS' },
{ name: 'Panama', value: 'PA' },
{ name: 'Papua New Guinea', value: 'PG' },
{ name: 'Paraguay', value: 'PY' },
{ name: 'Peru', value: 'PE' },
{ name: 'Philippines', value: 'PH' },
{ name: 'Pitcairn Islands', value: 'PN' },
{ name: 'Poland', value: 'PL' },
{ name: 'Portugal', value: 'PT' },
{ name: 'Puerto Rico', value: 'PR' },
{ name: 'Qatar', value: 'QA' },
{ name: 'Réunion', value: 'RE' },
{ name: 'Romania', value: 'RO' },
{ name: 'Russia', value: 'RU' },
{ name: 'Rwanda', value: 'RW' },
{ name: 'Samoa', value: 'WS' },
{ name: 'San Marino', value: 'SM' },
{ name: 'São Tomé & Príncipe', value: 'ST' },
{ name: 'Saudi Arabia', value: 'SA' },
{ name: 'Senegal', value: 'SN' },
{ name: 'Serbia', value: 'RS' },
{ name: 'Seychelles', value: 'SC' },
{ name: 'Sierra Leone', value: 'SL' },
{ name: 'Singapore', value: 'SG' },
{ name: 'Sint Maarten', value: 'SX' },
{ name: 'Slovakia', value: 'SK' },
{ name: 'Slovenia', value: 'SI' },
{ name: 'Solomon Islands', value: 'SB' },
{ name: 'Somalia', value: 'SO' },
{ name: 'South Africa', value: 'ZA' },
{ name: 'South Georgia & South Sandwich Islands', value: 'GS' },
{ name: 'South Korea', value: 'KR' },
{ name: 'South Sudan', value: 'SS' },
{ name: 'Spain', value: 'ES' },
{ name: 'Sri Lanka', value: 'LK' },
{ name: 'St. Barthélemy', value: 'BL' },
{ name: 'St. Helena', value: 'SH' },
{ name: 'St. Kitts & Nevis', value: 'KN' },
{ name: 'St. Lucia', value: 'LC' },
{ name: 'St. Martin', value: 'MF' },
{ name: 'St. Pierre & Miquelon', value: 'PM' },
{ name: 'St. Vincent & Grenadines', value: 'VC' },
{ name: 'Sudan', value: 'SD' },
{ name: 'Suriname', value: 'SR' },
{ name: 'Svalbard & Jan Mayen', value: 'SJ' },
{ name: 'Sweden', value: 'SE' },
{ name: 'Switzerland', value: 'CH' },
{ name: 'Syria', value: 'SY' },
{ name: 'Taiwan', value: 'TW' },
{ name: 'Tajikistan', value: 'TJ' },
{ name: 'Tanzania', value: 'TZ' },
{ name: 'Thailand', value: 'TH' },
{ name: 'Timor-Leste', value: 'TL' },
{ name: 'Togo', value: 'TG' },
{ name: 'Tokelau', value: 'TK' },
{ name: 'Tonga', value: 'TO' },
{ name: 'Trinidad & Tobago', value: 'TT' },
{ name: 'Tunisia', value: 'TN' },
{ name: 'Türkiye', value: 'TR' },
{ name: 'Turkmenistan', value: 'TM' },
{ name: 'Turks & Caicos Islands', value: 'TC' },
{ name: 'Tuvalu', value: 'TV' },
{ name: 'U.S. Outlying Islands', value: 'UM' },
{ name: 'U.S. Virgin Islands', value: 'VI' },
{ name: 'Uganda', value: 'UG' },
{ name: 'Ukraine', value: 'UA' },
{ name: 'United Arab Emirates', value: 'AE' },
{ name: 'United Kingdom', value: 'GB' },
{ name: 'United States', value: 'US' },
{ name: 'Uruguay', value: 'UY' },
{ name: 'Uzbekistan', value: 'UZ' },
{ name: 'Vanuatu', value: 'VU' },
{ name: 'Vatican City', value: 'VA' },
{ name: 'Venezuela', value: 'VE' },
{ name: 'Vietnam', value: 'VN' },
{ name: 'Wallis & Futuna', value: 'WF' },
{ name: 'Western Sahara', value: 'EH' },
{ name: 'Yemen', value: 'YE' },
{ name: 'Zambia', value: 'ZM' },
{ name: 'Zimbabwe', value: 'ZW' },
];
13 changes: 10 additions & 3 deletions nodes/Fedex/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SERVICE_TYPE_OPTIONS,
WEIGHT_UNIT_OPTIONS,
} from './constants';
import { COUNTRY_OPTIONS } from './countries';

// Presentational INodeProperties builders (ADR-0003: the field surface stays declarative).
// Roles namespace the shipper vs recipient parameters so both can live in one operation;
Expand Down Expand Up @@ -73,11 +74,12 @@ export function addressFields(role: Role, show: Show): INodeProperties[] {
displayOptions: { show },
},
{
displayName: `${cap(role)} Country Code`,
displayName: `${cap(role)} Country`,
name: `${role}CountryCode`,
type: 'string',
type: 'options',
options: COUNTRY_OPTIONS,
default: 'US',
description: 'Two-letter ISO country code',
description: 'Country of the address; the two-letter ISO code is what gets sent to FedEx',
displayOptions: { show },
},
];
Expand Down Expand Up @@ -176,6 +178,8 @@ const dimensionEntries: INodeProperties[] = [
type: 'number',
default: 0,
typeOptions: { minValue: 0 },
description:
'Dimensions are sent only when length, width, and height are all greater than zero',
},
{
displayName: 'Length',
Expand All @@ -192,6 +196,8 @@ const dimensionEntries: INodeProperties[] = [
type: 'number',
default: 0,
typeOptions: { minValue: 0 },
description:
'Dimensions are sent only when length, width, and height are all greater than zero',
},
];

Expand Down Expand Up @@ -244,6 +250,7 @@ const labelStockEntry: INodeProperties = {
// LABEL_STOCK_OPTIONS imported lazily below to keep the constant list co-located with Create.
options: [],
default: 'PAPER_4X6',
description: 'Paper size or thermal stock the label prints on. Defaults to the standard 4x6.',
};

/** Wrap a set of optional entries in the standard "Additional Fields" collection. */
Expand Down
11 changes: 11 additions & 0 deletions nodes/Fedex/resources/shipping/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ import {
const show = { resource: ['shipping'], operation: ['create'] };

export const createFields: INodeProperties[] = [
// Honest friction (cognitive-load audit, "What NOT to touch" #2): Create buys a real
// shipment and bills the configured account, and the node is usableAsTool, so an AI agent
// can invoke it. Surface the cost up front rather than burying it in docs.
{
displayName:
'Running this operation books a real FedEx shipment and bills the Shipping Account below. In Sandbox it is free; in Production it incurs charges.',
name: 'createCostNotice',
type: 'notice',
default: '',
displayOptions: { show },
},
accountNumberField(show),
...addressFields('shipper', show),
...contactFields('shipper', show),
Expand Down