@globalpayments-private/gpds-pattern-web 0.0.0-stage → 0.1.0-pr.7.gdcb74cd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +567 -2
- package/dist/cjs/control-strategy-DhhYTbse.js +113 -0
- package/dist/cjs/gpds-pattern-web.cjs.js +24 -0
- package/dist/cjs/index-Dzm6P89U.js +1565 -0
- package/dist/cjs/index.cjs.js +180 -0
- package/dist/cjs/loader.cjs.js +12 -0
- package/dist/cjs/uds-pattern-login-card_2.cjs.entry.js +1662 -0
- package/dist/collection/collection-manifest.json +14 -0
- package/dist/collection/components/uds-pattern-login-card/slimmers/identity-memory.slimmer.js +63 -0
- package/dist/collection/components/uds-pattern-login-card/slimmers/login-workflow.slimmer.js +357 -0
- package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-card-renderer.js +100 -0
- package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-field-renderer.js +129 -0
- package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-step-renderer.js +192 -0
- package/dist/collection/components/uds-pattern-login-card/slimmers/resend-throttle.slimmer.js +65 -0
- package/dist/collection/components/uds-pattern-login-card/uds-pattern-login-card.css +154 -0
- package/dist/collection/components/uds-pattern-login-card/uds-pattern-login-card.js +508 -0
- package/dist/collection/components/uds-pattern-login-card/validation/login-validation-rules.js +86 -0
- package/dist/collection/components/uds-pattern-login-card/view-model/login-card-handlers.js +1 -0
- package/dist/collection/components/uds-pattern-login-card/view-model/login-step.view-model.js +138 -0
- package/dist/collection/components/uds-pattern-login-page/uds-pattern-login-page.css +37 -0
- package/dist/collection/components/uds-pattern-login-page/uds-pattern-login-page.js +97 -0
- package/dist/collection/index.js +5 -0
- package/dist/collection/providers/auth-provider.js +28 -0
- package/dist/collection/providers/control-strategy.js +21 -0
- package/dist/collection/providers/login-config.js +4 -0
- package/dist/collection/providers/memory-auth-provider.js +133 -0
- package/dist/collection/providers/parse-auth-result.js +135 -0
- package/dist/collection/slimmer/common/log.js +14 -0
- package/dist/collection/slimmer/common/symbols.js +10 -0
- package/dist/collection/slimmer/common/types.js +2 -0
- package/dist/collection/slimmer/decorators/inject-slimmer.js +73 -0
- package/dist/collection/slimmer/decorators/map-to-component-field.js +41 -0
- package/dist/collection/slimmer/decorators/map-to-component-method.js +33 -0
- package/dist/collection/slimmer/decorators/slimmer.js +10 -0
- package/dist/collection/slimmer/index.js +5 -0
- package/dist/collection/utils/labels.js +81 -0
- package/dist/collection/utils/types.js +1 -0
- package/dist/components/index.d.ts +35 -0
- package/dist/components/index.js +1 -0
- package/dist/components/p-DOrg55po.js +1 -0
- package/dist/components/p-_G8A9gg0.js +1 -0
- package/dist/components/uds-pattern-login-card.d.ts +11 -0
- package/dist/components/uds-pattern-login-card.js +1 -0
- package/dist/components/uds-pattern-login-page.d.ts +11 -0
- package/dist/components/uds-pattern-login-page.js +1 -0
- package/dist/docs.d.ts +480 -0
- package/dist/docs.json +549 -0
- package/dist/esm/control-strategy-DOrg55po.js +105 -0
- package/dist/esm/gpds-pattern-web.js +20 -0
- package/dist/esm/index-DsXlLyGj.js +1537 -0
- package/dist/esm/index.js +170 -0
- package/dist/esm/loader.js +10 -0
- package/dist/esm/uds-pattern-login-card_2.entry.js +1659 -0
- package/dist/gpds-pattern-web/gpds-pattern-web.css +1 -0
- package/dist/gpds-pattern-web/gpds-pattern-web.esm.js +1 -0
- package/dist/gpds-pattern-web/index.esm.js +1 -0
- package/dist/gpds-pattern-web/p-2c97d2da.entry.js +1 -0
- package/dist/gpds-pattern-web/p-DOrg55po.js +1 -0
- package/dist/gpds-pattern-web/p-DsXlLyGj.js +3 -0
- package/dist/index.cjs.js +1 -0
- package/dist/index.js +1 -0
- package/dist/types/components/uds-pattern-login-card/slimmers/identity-memory.slimmer.d.ts +21 -0
- package/dist/types/components/uds-pattern-login-card/slimmers/login-workflow.slimmer.d.ts +100 -0
- package/dist/types/components/uds-pattern-login-card/slimmers/renderers/login-card-renderer.d.ts +44 -0
- package/dist/types/components/uds-pattern-login-card/slimmers/renderers/login-field-renderer.d.ts +91 -0
- package/dist/types/components/uds-pattern-login-card/slimmers/renderers/login-step-renderer.d.ts +40 -0
- package/dist/types/components/uds-pattern-login-card/slimmers/resend-throttle.slimmer.d.ts +21 -0
- package/dist/types/components/uds-pattern-login-card/uds-pattern-login-card.d.ts +134 -0
- package/dist/types/components/uds-pattern-login-card/validation/login-validation-rules.d.ts +72 -0
- package/dist/types/components/uds-pattern-login-card/view-model/login-card-handlers.d.ts +21 -0
- package/dist/types/components/uds-pattern-login-card/view-model/login-step.view-model.d.ts +96 -0
- package/dist/types/components/uds-pattern-login-page/uds-pattern-login-page.d.ts +21 -0
- package/dist/types/components.d.ts +327 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/providers/auth-provider.d.ts +168 -0
- package/dist/types/providers/control-strategy.d.ts +144 -0
- package/dist/types/providers/login-config.d.ts +52 -0
- package/dist/types/providers/memory-auth-provider.d.ts +37 -0
- package/dist/types/providers/parse-auth-result.d.ts +8 -0
- package/dist/types/slimmer/common/log.d.ts +8 -0
- package/dist/types/slimmer/common/symbols.d.ts +10 -0
- package/dist/types/slimmer/common/types.d.ts +30 -0
- package/dist/types/slimmer/decorators/inject-slimmer.d.ts +24 -0
- package/dist/types/slimmer/decorators/map-to-component-field.d.ts +16 -0
- package/dist/types/slimmer/decorators/map-to-component-method.d.ts +11 -0
- package/dist/types/slimmer/decorators/slimmer.d.ts +10 -0
- package/dist/types/slimmer/index.d.ts +6 -0
- package/dist/types/stencil-public-runtime.d.ts +1879 -0
- package/dist/types/utils/labels.d.ts +13 -0
- package/dist/types/utils/types.d.ts +106 -0
- package/dist/types/vega.d.ts +11 -0
- package/loader/cdn.js +1 -0
- package/loader/index.cjs.js +1 -0
- package/loader/index.d.ts +24 -0
- package/loader/index.es2017.js +1 -0
- package/loader/index.js +2 -0
- package/package.json +82 -4
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { formatLabel } from "../../../utils/labels";
|
|
2
|
+
/**
|
|
3
|
+
* The data a renderer needs to draw one step of the login workflow.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is a class with a factory, and not an inline object
|
|
6
|
+
*
|
|
7
|
+
* Deriving "what the user should see" from "what the workflow currently holds"
|
|
8
|
+
* is real logic: which affordances are available, what the heading says, which
|
|
9
|
+
* description variant applies, whether a countdown is running. Assembled inline
|
|
10
|
+
* inside `render()` that derivation is untestable and re-read on every change;
|
|
11
|
+
* named here it has one home, one test surface and one place to review.
|
|
12
|
+
*
|
|
13
|
+
* ## Why it carries no callbacks
|
|
14
|
+
*
|
|
15
|
+
* A view model is *data*. Handlers are supplied alongside it
|
|
16
|
+
* (`LoginCardHandlers`) rather than folded in, because a model that carries
|
|
17
|
+
* behaviour cannot be compared, snapshotted or reasoned about independently of
|
|
18
|
+
* the component that produced it. Keeping the two apart is what lets the
|
|
19
|
+
* renderers be checked as pure functions of their inputs.
|
|
20
|
+
*
|
|
21
|
+
* ## Pattern-library standard
|
|
22
|
+
*
|
|
23
|
+
* Every pattern component should follow this shape:
|
|
24
|
+
*
|
|
25
|
+
* - `view-model/<pattern>.view-model.ts` exporting a plain data class;
|
|
26
|
+
* - construction through a single `static from(state)` factory;
|
|
27
|
+
* - no inheritance, and no logic beyond derivation from the state passed in;
|
|
28
|
+
* - consumed by renderer slimmers, never mutated by them.
|
|
29
|
+
*
|
|
30
|
+
* Note this is a *view model*, not a DTO: it never crosses a process or
|
|
31
|
+
* serialisation boundary, only the boundary between the stateful layer
|
|
32
|
+
* (slimmers) and the rendering layer. DTO semantics — validation, wire format —
|
|
33
|
+
* belong at the provider seam, where data genuinely enters from outside.
|
|
34
|
+
*/
|
|
35
|
+
export class LoginStepViewModel {
|
|
36
|
+
constructor(
|
|
37
|
+
/** The step being rendered. */
|
|
38
|
+
step,
|
|
39
|
+
/** Resolved copy, after merging any consumer overrides. */
|
|
40
|
+
copy,
|
|
41
|
+
/** Heading for this step. */
|
|
42
|
+
heading,
|
|
43
|
+
/** Supporting description, when this step has one. */
|
|
44
|
+
description,
|
|
45
|
+
/** Error copy to surface, already mapped to approved wording. */
|
|
46
|
+
errorText,
|
|
47
|
+
/** Whether a provider call is in flight. */
|
|
48
|
+
busy,
|
|
49
|
+
/** Which affordances the provider can service. */
|
|
50
|
+
can,
|
|
51
|
+
/** MFA methods offered by the provider. */
|
|
52
|
+
methods,
|
|
53
|
+
/** Values collected on the current step. */
|
|
54
|
+
values,
|
|
55
|
+
/** How many characters the passcode entry expects. */
|
|
56
|
+
otpLength,
|
|
57
|
+
/** Seconds left on the resend throttle; zero when resending is allowed. */
|
|
58
|
+
resendSeconds) {
|
|
59
|
+
this.step = step;
|
|
60
|
+
this.copy = copy;
|
|
61
|
+
this.heading = heading;
|
|
62
|
+
this.description = description;
|
|
63
|
+
this.errorText = errorText;
|
|
64
|
+
this.busy = busy;
|
|
65
|
+
this.can = can;
|
|
66
|
+
this.methods = methods;
|
|
67
|
+
this.values = values;
|
|
68
|
+
this.otpLength = otpLength;
|
|
69
|
+
this.resendSeconds = resendSeconds;
|
|
70
|
+
}
|
|
71
|
+
static from(state) {
|
|
72
|
+
const { step, copy } = state;
|
|
73
|
+
return new LoginStepViewModel(step, copy, headingFor(step, copy), descriptionFor(step, copy, state), state.errorText, state.busy, state.can, state.methods, state.values, state.otpLength, state.resendSeconds);
|
|
74
|
+
}
|
|
75
|
+
/** Steps that render no form: terminal outcomes and confirmations. */
|
|
76
|
+
get isTerminal() {
|
|
77
|
+
return this.step === 'success' || this.step === 'locked' || this.step === 'check-email';
|
|
78
|
+
}
|
|
79
|
+
/** Whether a resend is currently throttled. */
|
|
80
|
+
get isResendThrottled() {
|
|
81
|
+
return this.resendSeconds > 0;
|
|
82
|
+
}
|
|
83
|
+
/** Copy for the throttle countdown, with the remaining seconds applied. */
|
|
84
|
+
get resendCountdownText() {
|
|
85
|
+
return formatLabel(this.copy.resendCodeIn, { seconds: this.resendSeconds });
|
|
86
|
+
}
|
|
87
|
+
/** The current value of a collected field, as a string. */
|
|
88
|
+
valueOf(name) {
|
|
89
|
+
const value = this.values[name];
|
|
90
|
+
return typeof value === 'string' ? value : '';
|
|
91
|
+
}
|
|
92
|
+
/** Whether a boolean field is set. */
|
|
93
|
+
isChecked(name) {
|
|
94
|
+
return this.values[name] === true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function headingFor(step, copy) {
|
|
98
|
+
const headings = {
|
|
99
|
+
identifier: copy.identifierHeading,
|
|
100
|
+
password: copy.passwordHeading,
|
|
101
|
+
'mfa-method': copy.mfaMethodHeading,
|
|
102
|
+
otp: copy.otpHeading,
|
|
103
|
+
'forgot-password': copy.forgotPasswordHeading,
|
|
104
|
+
'forgot-username': copy.forgotUsernameHeading,
|
|
105
|
+
'reset-password': copy.resetPasswordHeading,
|
|
106
|
+
'sign-up': copy.signUpHeading,
|
|
107
|
+
'check-email': copy.checkEmailHeading,
|
|
108
|
+
locked: copy.lockedHeading,
|
|
109
|
+
success: copy.successHeading,
|
|
110
|
+
};
|
|
111
|
+
return headings[step];
|
|
112
|
+
}
|
|
113
|
+
function descriptionFor(step, copy, ctx) {
|
|
114
|
+
switch (step) {
|
|
115
|
+
case 'mfa-method':
|
|
116
|
+
return copy.mfaMethodDescription;
|
|
117
|
+
case 'otp':
|
|
118
|
+
return ctx.otpMethodLabel
|
|
119
|
+
? formatLabel(copy.otpDescriptionForMethod, { method: ctx.otpMethodLabel })
|
|
120
|
+
: copy.otpDescription;
|
|
121
|
+
case 'forgot-password':
|
|
122
|
+
return copy.forgotPasswordDescription;
|
|
123
|
+
case 'forgot-username':
|
|
124
|
+
return copy.forgotUsernameDescription;
|
|
125
|
+
case 'sign-up':
|
|
126
|
+
return copy.signUpDescription;
|
|
127
|
+
case 'check-email':
|
|
128
|
+
return ctx.destination
|
|
129
|
+
? formatLabel(copy.checkEmailDescriptionWithDestination, { destination: ctx.destination })
|
|
130
|
+
: copy.checkEmailDescription;
|
|
131
|
+
case 'locked':
|
|
132
|
+
return ctx.unlockMinutes
|
|
133
|
+
? formatLabel(copy.lockedDescriptionWithTime, { minutes: ctx.unlockMinutes })
|
|
134
|
+
: copy.lockedDescription;
|
|
135
|
+
default:
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
min-height: 100vh;
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
font-family: var(--uds-pattern-font-family);
|
|
7
|
+
color: var(--uds-pattern-color-text);
|
|
8
|
+
background: var(--uds-pattern-color-background);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.masthead {
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
align-items: center;
|
|
15
|
+
padding: var(--uds-pattern-space-md) var(--uds-pattern-space-lg);
|
|
16
|
+
background: var(--uds-pattern-color-surface);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.masthead__logo {
|
|
20
|
+
display: block;
|
|
21
|
+
max-height: 2rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.content {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex: 1;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
align-items: center;
|
|
29
|
+
padding: var(--uds-pattern-space-xl) var(--uds-pattern-space-md);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.page-footer:not(:empty) {
|
|
33
|
+
padding: var(--uds-pattern-space-md) var(--uds-pattern-space-lg);
|
|
34
|
+
text-align: center;
|
|
35
|
+
font-size: var(--uds-pattern-font-size-sm);
|
|
36
|
+
color: var(--uds-pattern-color-text-subtle);
|
|
37
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { h, Host } from "@stencil/core";
|
|
2
|
+
/**
|
|
3
|
+
* Full-page layout shell for authentication screens: branded masthead, a
|
|
4
|
+
* centred content column sized for `uds-pattern-login-card`, and an optional legal
|
|
5
|
+
* footer.
|
|
6
|
+
*
|
|
7
|
+
* It exists so consuming portals do not re-implement the same centred,
|
|
8
|
+
* responsive, brandable login layout for every product.
|
|
9
|
+
*
|
|
10
|
+
* @slot brand - Replaces the default masthead logo (e.g. a partner lockup).
|
|
11
|
+
* @slot - The authentication card, typically a `uds-pattern-login-card`.
|
|
12
|
+
* @slot footer - Legal / utility links rendered beneath the content column.
|
|
13
|
+
*/
|
|
14
|
+
export class UdsPatternLoginPage {
|
|
15
|
+
constructor() {
|
|
16
|
+
/** Accessible name for the masthead logo. */
|
|
17
|
+
this.logoAlt = '';
|
|
18
|
+
}
|
|
19
|
+
render() {
|
|
20
|
+
const logo = this.logoSrc ? h("img", { class: "masthead__logo", src: this.logoSrc, alt: this.logoAlt }) : null;
|
|
21
|
+
return (h(Host, { key: '7c2d0d6ae80dc7e52c0780c457de982738ef8faa' }, h("header", { key: '7eac231827e53a5e4018591506f7632aff8ddda5', class: "masthead" }, h("slot", { key: 'e80b31f6301079a15f45066189c646ed4c444e24', name: "brand" }, this.logoHref && logo ? h("a", { href: this.logoHref }, logo) : logo)), h("main", { key: '3081bf6ec017cff44900f6cd0d9c98b38baf5126', class: "content" }, h("slot", { key: '3c84048fae834578b00254e6bdbad4eecc74b5e2' })), h("footer", { key: 'dfb064a71e3f39c8e99212b523d043f9f9f20538', class: "page-footer" }, h("slot", { key: '5ea9ef897c20a96f3c42b2b4f54496155516a423', name: "footer" }))));
|
|
22
|
+
}
|
|
23
|
+
static get is() { return "uds-pattern-login-page"; }
|
|
24
|
+
static get encapsulation() { return "shadow"; }
|
|
25
|
+
static get originalStyleUrls() {
|
|
26
|
+
return {
|
|
27
|
+
"$": ["uds-pattern-login-page.css"]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
static get styleUrls() {
|
|
31
|
+
return {
|
|
32
|
+
"$": ["uds-pattern-login-page.css"]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
static get properties() {
|
|
36
|
+
return {
|
|
37
|
+
"logoSrc": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"mutable": false,
|
|
40
|
+
"complexType": {
|
|
41
|
+
"original": "string",
|
|
42
|
+
"resolved": "string | undefined",
|
|
43
|
+
"references": {}
|
|
44
|
+
},
|
|
45
|
+
"required": false,
|
|
46
|
+
"optional": true,
|
|
47
|
+
"docs": {
|
|
48
|
+
"tags": [],
|
|
49
|
+
"text": "URL of the masthead logo. Ignored when the `brand` slot is used."
|
|
50
|
+
},
|
|
51
|
+
"getter": false,
|
|
52
|
+
"setter": false,
|
|
53
|
+
"reflect": false,
|
|
54
|
+
"attribute": "logo-src"
|
|
55
|
+
},
|
|
56
|
+
"logoAlt": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"mutable": false,
|
|
59
|
+
"complexType": {
|
|
60
|
+
"original": "string",
|
|
61
|
+
"resolved": "string",
|
|
62
|
+
"references": {}
|
|
63
|
+
},
|
|
64
|
+
"required": false,
|
|
65
|
+
"optional": false,
|
|
66
|
+
"docs": {
|
|
67
|
+
"tags": [],
|
|
68
|
+
"text": "Accessible name for the masthead logo."
|
|
69
|
+
},
|
|
70
|
+
"getter": false,
|
|
71
|
+
"setter": false,
|
|
72
|
+
"reflect": false,
|
|
73
|
+
"attribute": "logo-alt",
|
|
74
|
+
"defaultValue": "''"
|
|
75
|
+
},
|
|
76
|
+
"logoHref": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"mutable": false,
|
|
79
|
+
"complexType": {
|
|
80
|
+
"original": "string",
|
|
81
|
+
"resolved": "string | undefined",
|
|
82
|
+
"references": {}
|
|
83
|
+
},
|
|
84
|
+
"required": false,
|
|
85
|
+
"optional": true,
|
|
86
|
+
"docs": {
|
|
87
|
+
"tags": [],
|
|
88
|
+
"text": "Destination for the masthead logo link. When unset the logo is not a link."
|
|
89
|
+
},
|
|
90
|
+
"getter": false,
|
|
91
|
+
"setter": false,
|
|
92
|
+
"reflect": false,
|
|
93
|
+
"attribute": "logo-href"
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DEFAULT_LOGIN_LABELS, formatLabel } from './utils/labels';
|
|
2
|
+
export { createAuthProvider } from './providers/auth-provider';
|
|
3
|
+
export { createMemoryAuthProvider } from './providers/memory-auth-provider';
|
|
4
|
+
export { createControlStrategy, fail, goTo, stay } from './providers/control-strategy';
|
|
5
|
+
export { createLoginConfig } from './providers/login-config';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider contracts for the GP pattern library.
|
|
3
|
+
*
|
|
4
|
+
* A pattern owns the *workflow* — which step comes next, what is in flight,
|
|
5
|
+
* what the user is told when something fails. It owns no SDK code and knows no
|
|
6
|
+
* identity platform. The seam between the two is a Provider contract: the
|
|
7
|
+
* consumer brings their own backend SDK (Okta, Auth0, Cognito, Ping, GCP API
|
|
8
|
+
* Gateway, Azure APIM, …) and writes a thin adapter against the interface
|
|
9
|
+
* below.
|
|
10
|
+
*
|
|
11
|
+
* See `docs/adr/0002-provider-driven-patterns.md` for why the workflow lives
|
|
12
|
+
* inside the pattern rather than in the host application.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Identity helper that gives adapter authors type inference and a single place
|
|
16
|
+
* to add shared behaviour later (telemetry, retry, logging) without changing
|
|
17
|
+
* every call site.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const auth = createAuthProvider({
|
|
21
|
+
* signIn: ({ identifier, password }) => oktaSdk.authn(identifier, password),
|
|
22
|
+
* verifyOtp: ({ code }) => oktaSdk.verifyFactor(code),
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function createAuthProvider(provider) {
|
|
27
|
+
return provider;
|
|
28
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Moves the workflow to `step`. */
|
|
2
|
+
export const goTo = (step) => ({ type: 'goto', step });
|
|
3
|
+
/** Leaves the workflow where it is. */
|
|
4
|
+
export const stay = () => ({ type: 'stay' });
|
|
5
|
+
/** Surfaces a failure without moving. */
|
|
6
|
+
export const fail = (code, message) => ({
|
|
7
|
+
type: 'fail',
|
|
8
|
+
code,
|
|
9
|
+
message,
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Runs a checkpoint through the strategy, falling back to the pattern's own
|
|
13
|
+
* behaviour when the strategy does not intercept it.
|
|
14
|
+
*/
|
|
15
|
+
export function intercept(interceptor, context, fallback) {
|
|
16
|
+
return interceptor ? interceptor(context, fallback) : fallback();
|
|
17
|
+
}
|
|
18
|
+
/** Identity helper giving a strategy literal its types without a type annotation. */
|
|
19
|
+
export function createControlStrategy(strategy) {
|
|
20
|
+
return strategy;
|
|
21
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createAuthProvider } from "./auth-provider";
|
|
2
|
+
const DEFAULT_USER = {
|
|
3
|
+
identifier: 'ada@example.com',
|
|
4
|
+
password: 'correct-horse',
|
|
5
|
+
mfaMethods: [
|
|
6
|
+
{ id: 'app', label: 'Authenticator app', description: 'Use the code from your authenticator app' },
|
|
7
|
+
{ id: 'sms', label: 'Text message', description: 'Ending in ••3312' },
|
|
8
|
+
],
|
|
9
|
+
otpCode: '555555',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* An in-memory reference implementation of {@link UdsAuthProvider}.
|
|
13
|
+
*
|
|
14
|
+
* It exists so the demo harness and the test suite can drive the *real*
|
|
15
|
+
* workflow — identifier → password → MFA choice → OTP → success, plus the
|
|
16
|
+
* recovery, lockout and sign-up branches — without a network or an identity
|
|
17
|
+
* platform. It is also the worked example adapter authors copy from.
|
|
18
|
+
*
|
|
19
|
+
* Not intended for production use: it compares passwords in plain text.
|
|
20
|
+
*/
|
|
21
|
+
export function createMemoryAuthProvider(options = {}) {
|
|
22
|
+
const users = options.users ?? [DEFAULT_USER];
|
|
23
|
+
const maxAttempts = options.maxAttempts ?? 3;
|
|
24
|
+
const resendAfterMs = options.resendAfterMs ?? 30000;
|
|
25
|
+
const caps = {
|
|
26
|
+
signUp: true,
|
|
27
|
+
requestPasswordReset: true,
|
|
28
|
+
requestUsernameReminder: true,
|
|
29
|
+
resendOtp: true,
|
|
30
|
+
...options.capabilities,
|
|
31
|
+
};
|
|
32
|
+
// Server-side state of the in-flight attempt.
|
|
33
|
+
let pending;
|
|
34
|
+
let failures = 0;
|
|
35
|
+
let locked = false;
|
|
36
|
+
const find = (identifier) => users.find(u => u.identifier.toLowerCase() === identifier.trim().toLowerCase());
|
|
37
|
+
const afterPassword = (user) => {
|
|
38
|
+
failures = 0;
|
|
39
|
+
if (user.mustResetPassword) {
|
|
40
|
+
return { status: 'password_reset_required' };
|
|
41
|
+
}
|
|
42
|
+
if (user.mfaMethods?.length) {
|
|
43
|
+
return { status: 'mfa_required', methods: user.mfaMethods };
|
|
44
|
+
}
|
|
45
|
+
return { status: 'success', session: { identifier: user.identifier } };
|
|
46
|
+
};
|
|
47
|
+
const provider = {
|
|
48
|
+
async signIn({ identifier, password }) {
|
|
49
|
+
if (locked) {
|
|
50
|
+
return { status: 'locked' };
|
|
51
|
+
}
|
|
52
|
+
const user = find(identifier);
|
|
53
|
+
if (!user) {
|
|
54
|
+
return { status: 'error', code: 'account_not_found' };
|
|
55
|
+
}
|
|
56
|
+
pending = user;
|
|
57
|
+
// Identifier-first: no password supplied yet.
|
|
58
|
+
if (password === undefined) {
|
|
59
|
+
return { status: 'password_required' };
|
|
60
|
+
}
|
|
61
|
+
return provider.verifyPassword({ identifier, password });
|
|
62
|
+
},
|
|
63
|
+
async verifyPassword({ password }) {
|
|
64
|
+
if (locked) {
|
|
65
|
+
return { status: 'locked' };
|
|
66
|
+
}
|
|
67
|
+
if (!pending) {
|
|
68
|
+
return { status: 'error', code: 'unknown' };
|
|
69
|
+
}
|
|
70
|
+
if (password !== pending.password) {
|
|
71
|
+
failures += 1;
|
|
72
|
+
if (failures >= maxAttempts) {
|
|
73
|
+
locked = true;
|
|
74
|
+
return { status: 'locked', unlockAfterMs: 15 * 60000 };
|
|
75
|
+
}
|
|
76
|
+
return { status: 'error', code: 'invalid_credentials' };
|
|
77
|
+
}
|
|
78
|
+
return afterPassword(pending);
|
|
79
|
+
},
|
|
80
|
+
async selectMfaMethod({ methodId }) {
|
|
81
|
+
const method = pending?.mfaMethods?.find(m => m.id === methodId);
|
|
82
|
+
return { status: 'otp_required', method, codeLength: 6, resendAfterMs };
|
|
83
|
+
},
|
|
84
|
+
async verifyOtp({ code }) {
|
|
85
|
+
if (code !== (pending?.otpCode ?? DEFAULT_USER.otpCode)) {
|
|
86
|
+
failures += 1;
|
|
87
|
+
if (failures >= maxAttempts) {
|
|
88
|
+
locked = true;
|
|
89
|
+
return { status: 'locked' };
|
|
90
|
+
}
|
|
91
|
+
return { status: 'error', code: 'invalid_code' };
|
|
92
|
+
}
|
|
93
|
+
failures = 0;
|
|
94
|
+
return { status: 'success', session: { identifier: pending?.identifier } };
|
|
95
|
+
},
|
|
96
|
+
async resetPassword({ password }) {
|
|
97
|
+
if (password.length < 8) {
|
|
98
|
+
return { status: 'error', code: 'password_policy' };
|
|
99
|
+
}
|
|
100
|
+
if (pending) {
|
|
101
|
+
pending.password = password;
|
|
102
|
+
pending.mustResetPassword = false;
|
|
103
|
+
}
|
|
104
|
+
return { status: 'success', session: { identifier: pending?.identifier } };
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
if (caps.resendOtp) {
|
|
108
|
+
provider.resendOtp = async () => ({ status: 'otp_required', codeLength: 6, resendAfterMs });
|
|
109
|
+
}
|
|
110
|
+
if (caps.requestPasswordReset) {
|
|
111
|
+
provider.requestPasswordReset = async ({ identifier }) => {
|
|
112
|
+
const user = find(identifier);
|
|
113
|
+
return user
|
|
114
|
+
? { status: 'reset_link_sent', destination: user.identifier }
|
|
115
|
+
: { status: 'error', code: 'account_not_found' };
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (caps.requestUsernameReminder) {
|
|
119
|
+
provider.requestUsernameReminder = async ({ email }) => ({ status: 'username_sent', destination: email });
|
|
120
|
+
}
|
|
121
|
+
if (caps.signUp) {
|
|
122
|
+
provider.signUp = async ({ firstName, lastName, identifier }) => {
|
|
123
|
+
if (find(identifier)) {
|
|
124
|
+
return { status: 'error', code: 'account_exists' };
|
|
125
|
+
}
|
|
126
|
+
const user = { identifier, password: '', otpCode: DEFAULT_USER.otpCode };
|
|
127
|
+
users.push(user);
|
|
128
|
+
pending = user;
|
|
129
|
+
return { status: 'success', session: { identifier, firstName, lastName } };
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return createAuthProvider(provider);
|
|
133
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime validation for the one boundary where data genuinely enters the
|
|
3
|
+
* pattern from outside: a provider's `UdsAuthResult`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why validate here and nowhere else
|
|
6
|
+
*
|
|
7
|
+
* `UdsAuthResult` is a *DTO* in the strict sense — it crosses a trust boundary.
|
|
8
|
+
* An adapter is written by a consuming team, usually by mapping a vendor SDK's
|
|
9
|
+
* response, and TypeScript's guarantees stop at the seam: a compiled adapter
|
|
10
|
+
* can still hand back `{ status: 'otp_requred' }` from a mistyped mapping, or
|
|
11
|
+
* `{ status: 'mfa_required' }` with no `methods` because the vendor returned
|
|
12
|
+
* `null`.
|
|
13
|
+
*
|
|
14
|
+
* Unvalidated, those land as *silence*. The workflow's `switch` falls through,
|
|
15
|
+
* the card stays on its current step, no error is raised, and the user sees a
|
|
16
|
+
* button that does nothing. Debugging that means reading our source. Validated,
|
|
17
|
+
* the same mistake names itself at the point it was made.
|
|
18
|
+
*
|
|
19
|
+
* The view model deliberately has no equivalent: it never crosses a boundary,
|
|
20
|
+
* only moves between layers that are compiled together, so checking it at
|
|
21
|
+
* runtime would buy nothing the compiler has not already proven.
|
|
22
|
+
*
|
|
23
|
+
* ## Failure policy
|
|
24
|
+
*
|
|
25
|
+
* A malformed result is converted into an `unknown` error — the user always
|
|
26
|
+
* sees approved copy — and reported on the console with the offending value.
|
|
27
|
+
* The pattern never throws into a consumer's promise chain for this.
|
|
28
|
+
*/
|
|
29
|
+
const RESULT_STATUSES = [
|
|
30
|
+
'success',
|
|
31
|
+
'password_required',
|
|
32
|
+
'mfa_required',
|
|
33
|
+
'otp_required',
|
|
34
|
+
'password_reset_required',
|
|
35
|
+
'reset_link_sent',
|
|
36
|
+
'username_sent',
|
|
37
|
+
'locked',
|
|
38
|
+
'error',
|
|
39
|
+
];
|
|
40
|
+
const ERROR_CODES = [
|
|
41
|
+
'invalid_credentials',
|
|
42
|
+
'invalid_identifier',
|
|
43
|
+
'invalid_code',
|
|
44
|
+
'code_expired',
|
|
45
|
+
'account_locked',
|
|
46
|
+
'account_not_found',
|
|
47
|
+
'account_exists',
|
|
48
|
+
'password_policy',
|
|
49
|
+
'passwords_do_not_match',
|
|
50
|
+
'rate_limited',
|
|
51
|
+
'network',
|
|
52
|
+
'unknown',
|
|
53
|
+
];
|
|
54
|
+
/** Thrown only internally; never surfaces to a consumer. */
|
|
55
|
+
class InvalidAuthResultError extends Error {
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Validates a provider's response, returning it narrowed to `UdsAuthResult`.
|
|
59
|
+
*
|
|
60
|
+
* @param value what the adapter returned
|
|
61
|
+
* @param source the provider method called, for the diagnostic message
|
|
62
|
+
*/
|
|
63
|
+
export function parseAuthResult(value, source) {
|
|
64
|
+
try {
|
|
65
|
+
return assertAuthResult(value, source);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (!(error instanceof InvalidAuthResultError)) {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
// eslint-disable-next-line no-console
|
|
72
|
+
console.error(`[uds-pattern-login-card] ${error.message}`, value);
|
|
73
|
+
return { status: 'error', code: 'unknown' };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function assertAuthResult(value, source) {
|
|
77
|
+
const result = value;
|
|
78
|
+
if (!result || typeof result !== 'object') {
|
|
79
|
+
throw new InvalidAuthResultError(`${source} resolved with ${describe(value)}, expected a UdsAuthResult object.`);
|
|
80
|
+
}
|
|
81
|
+
const status = result.status;
|
|
82
|
+
if (!RESULT_STATUSES.includes(status)) {
|
|
83
|
+
throw new InvalidAuthResultError(`${source} resolved with status ${describe(status)}. Expected one of: ${RESULT_STATUSES.join(', ')}.`);
|
|
84
|
+
}
|
|
85
|
+
switch (status) {
|
|
86
|
+
case 'mfa_required':
|
|
87
|
+
assertMethods(result.methods, source);
|
|
88
|
+
break;
|
|
89
|
+
case 'otp_required':
|
|
90
|
+
if (result.method !== undefined) {
|
|
91
|
+
assertMethod(result.method, source, 'method');
|
|
92
|
+
}
|
|
93
|
+
assertOptionalNumber(result.codeLength, source, 'codeLength');
|
|
94
|
+
assertOptionalNumber(result.resendAfterMs, source, 'resendAfterMs');
|
|
95
|
+
break;
|
|
96
|
+
case 'locked':
|
|
97
|
+
assertOptionalNumber(result.unlockAfterMs, source, 'unlockAfterMs');
|
|
98
|
+
break;
|
|
99
|
+
case 'error':
|
|
100
|
+
if (!ERROR_CODES.includes(result.code)) {
|
|
101
|
+
throw new InvalidAuthResultError(`${source} resolved with error code ${describe(result.code)}. Expected one of: ${ERROR_CODES.join(', ')}.`);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
function assertMethods(value, source) {
|
|
108
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
109
|
+
throw new InvalidAuthResultError(`${source} resolved with status 'mfa_required' but methods was ${describe(value)}. ` +
|
|
110
|
+
'Expected a non-empty array of UdsMfaMethod — the card has nothing to offer the user otherwise.');
|
|
111
|
+
}
|
|
112
|
+
value.forEach((method, index) => assertMethod(method, source, `methods[${index}]`));
|
|
113
|
+
}
|
|
114
|
+
function assertMethod(value, source, path) {
|
|
115
|
+
const method = value;
|
|
116
|
+
if (!method || typeof method !== 'object' || typeof method.id !== 'string' || typeof method.label !== 'string') {
|
|
117
|
+
throw new InvalidAuthResultError(`${source} resolved with ${path} = ${describe(value)}. Expected { id: string, label: string }.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function assertOptionalNumber(value, source, path) {
|
|
121
|
+
if (value !== undefined && (typeof value !== 'number' || !Number.isFinite(value))) {
|
|
122
|
+
throw new InvalidAuthResultError(`${source} resolved with ${path} = ${describe(value)}. Expected a finite number or undefined.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function describe(value) {
|
|
126
|
+
if (value === null)
|
|
127
|
+
return 'null';
|
|
128
|
+
if (value === undefined)
|
|
129
|
+
return 'undefined';
|
|
130
|
+
if (typeof value === 'string')
|
|
131
|
+
return `'${value}'`;
|
|
132
|
+
if (Array.isArray(value))
|
|
133
|
+
return `an array of ${value.length}`;
|
|
134
|
+
return typeof value === 'object' ? 'an object' : String(value);
|
|
135
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Console wrapper carrying a GP prefix, so framework diagnostics are traceable. */
|
|
2
|
+
export class LogUtility {
|
|
3
|
+
static log(...message) {
|
|
4
|
+
console.log(`%c${this.PREFIX}`, this.PREFIX_STYLE, ...message);
|
|
5
|
+
}
|
|
6
|
+
static warn(...message) {
|
|
7
|
+
console.warn(`%c${this.PREFIX}`, this.PREFIX_STYLE, ...message);
|
|
8
|
+
}
|
|
9
|
+
static error(...message) {
|
|
10
|
+
console.error(`%c${this.PREFIX}`, this.PREFIX_STYLE, ...message);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
LogUtility.PREFIX = '[GP]';
|
|
14
|
+
LogUtility.PREFIX_STYLE = 'font-weight: bold';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal markers used to attach slimmer wiring to component and slimmer
|
|
3
|
+
* instances without colliding with any real field name.
|
|
4
|
+
*/
|
|
5
|
+
/** On a slimmer prototype: the list of component methods it hooks into. */
|
|
6
|
+
export const SlimmerMappedMethod = Symbol('SlimmerMappedMethod');
|
|
7
|
+
/** On a slimmer instance: the component that injected it. */
|
|
8
|
+
export const SlimmerConsumedBy = Symbol('SlimmerConsumedBy');
|
|
9
|
+
/** On a component instance: the slimmers it has injected, keyed by field name. */
|
|
10
|
+
export const ConsumerSlimmers = Symbol('ConsumerSlimmers');
|