@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.
Files changed (97) hide show
  1. package/README.md +567 -2
  2. package/dist/cjs/control-strategy-DhhYTbse.js +113 -0
  3. package/dist/cjs/gpds-pattern-web.cjs.js +24 -0
  4. package/dist/cjs/index-Dzm6P89U.js +1565 -0
  5. package/dist/cjs/index.cjs.js +180 -0
  6. package/dist/cjs/loader.cjs.js +12 -0
  7. package/dist/cjs/uds-pattern-login-card_2.cjs.entry.js +1662 -0
  8. package/dist/collection/collection-manifest.json +14 -0
  9. package/dist/collection/components/uds-pattern-login-card/slimmers/identity-memory.slimmer.js +63 -0
  10. package/dist/collection/components/uds-pattern-login-card/slimmers/login-workflow.slimmer.js +357 -0
  11. package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-card-renderer.js +100 -0
  12. package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-field-renderer.js +129 -0
  13. package/dist/collection/components/uds-pattern-login-card/slimmers/renderers/login-step-renderer.js +192 -0
  14. package/dist/collection/components/uds-pattern-login-card/slimmers/resend-throttle.slimmer.js +65 -0
  15. package/dist/collection/components/uds-pattern-login-card/uds-pattern-login-card.css +154 -0
  16. package/dist/collection/components/uds-pattern-login-card/uds-pattern-login-card.js +508 -0
  17. package/dist/collection/components/uds-pattern-login-card/validation/login-validation-rules.js +86 -0
  18. package/dist/collection/components/uds-pattern-login-card/view-model/login-card-handlers.js +1 -0
  19. package/dist/collection/components/uds-pattern-login-card/view-model/login-step.view-model.js +138 -0
  20. package/dist/collection/components/uds-pattern-login-page/uds-pattern-login-page.css +37 -0
  21. package/dist/collection/components/uds-pattern-login-page/uds-pattern-login-page.js +97 -0
  22. package/dist/collection/index.js +5 -0
  23. package/dist/collection/providers/auth-provider.js +28 -0
  24. package/dist/collection/providers/control-strategy.js +21 -0
  25. package/dist/collection/providers/login-config.js +4 -0
  26. package/dist/collection/providers/memory-auth-provider.js +133 -0
  27. package/dist/collection/providers/parse-auth-result.js +135 -0
  28. package/dist/collection/slimmer/common/log.js +14 -0
  29. package/dist/collection/slimmer/common/symbols.js +10 -0
  30. package/dist/collection/slimmer/common/types.js +2 -0
  31. package/dist/collection/slimmer/decorators/inject-slimmer.js +73 -0
  32. package/dist/collection/slimmer/decorators/map-to-component-field.js +41 -0
  33. package/dist/collection/slimmer/decorators/map-to-component-method.js +33 -0
  34. package/dist/collection/slimmer/decorators/slimmer.js +10 -0
  35. package/dist/collection/slimmer/index.js +5 -0
  36. package/dist/collection/utils/labels.js +81 -0
  37. package/dist/collection/utils/types.js +1 -0
  38. package/dist/components/index.d.ts +35 -0
  39. package/dist/components/index.js +1 -0
  40. package/dist/components/p-DOrg55po.js +1 -0
  41. package/dist/components/p-_G8A9gg0.js +1 -0
  42. package/dist/components/uds-pattern-login-card.d.ts +11 -0
  43. package/dist/components/uds-pattern-login-card.js +1 -0
  44. package/dist/components/uds-pattern-login-page.d.ts +11 -0
  45. package/dist/components/uds-pattern-login-page.js +1 -0
  46. package/dist/docs.d.ts +480 -0
  47. package/dist/docs.json +549 -0
  48. package/dist/esm/control-strategy-DOrg55po.js +105 -0
  49. package/dist/esm/gpds-pattern-web.js +20 -0
  50. package/dist/esm/index-DsXlLyGj.js +1537 -0
  51. package/dist/esm/index.js +170 -0
  52. package/dist/esm/loader.js +10 -0
  53. package/dist/esm/uds-pattern-login-card_2.entry.js +1659 -0
  54. package/dist/gpds-pattern-web/gpds-pattern-web.css +1 -0
  55. package/dist/gpds-pattern-web/gpds-pattern-web.esm.js +1 -0
  56. package/dist/gpds-pattern-web/index.esm.js +1 -0
  57. package/dist/gpds-pattern-web/p-2c97d2da.entry.js +1 -0
  58. package/dist/gpds-pattern-web/p-DOrg55po.js +1 -0
  59. package/dist/gpds-pattern-web/p-DsXlLyGj.js +3 -0
  60. package/dist/index.cjs.js +1 -0
  61. package/dist/index.js +1 -0
  62. package/dist/types/components/uds-pattern-login-card/slimmers/identity-memory.slimmer.d.ts +21 -0
  63. package/dist/types/components/uds-pattern-login-card/slimmers/login-workflow.slimmer.d.ts +100 -0
  64. package/dist/types/components/uds-pattern-login-card/slimmers/renderers/login-card-renderer.d.ts +44 -0
  65. package/dist/types/components/uds-pattern-login-card/slimmers/renderers/login-field-renderer.d.ts +91 -0
  66. package/dist/types/components/uds-pattern-login-card/slimmers/renderers/login-step-renderer.d.ts +40 -0
  67. package/dist/types/components/uds-pattern-login-card/slimmers/resend-throttle.slimmer.d.ts +21 -0
  68. package/dist/types/components/uds-pattern-login-card/uds-pattern-login-card.d.ts +134 -0
  69. package/dist/types/components/uds-pattern-login-card/validation/login-validation-rules.d.ts +72 -0
  70. package/dist/types/components/uds-pattern-login-card/view-model/login-card-handlers.d.ts +21 -0
  71. package/dist/types/components/uds-pattern-login-card/view-model/login-step.view-model.d.ts +96 -0
  72. package/dist/types/components/uds-pattern-login-page/uds-pattern-login-page.d.ts +21 -0
  73. package/dist/types/components.d.ts +327 -0
  74. package/dist/types/index.d.ts +10 -0
  75. package/dist/types/providers/auth-provider.d.ts +168 -0
  76. package/dist/types/providers/control-strategy.d.ts +144 -0
  77. package/dist/types/providers/login-config.d.ts +52 -0
  78. package/dist/types/providers/memory-auth-provider.d.ts +37 -0
  79. package/dist/types/providers/parse-auth-result.d.ts +8 -0
  80. package/dist/types/slimmer/common/log.d.ts +8 -0
  81. package/dist/types/slimmer/common/symbols.d.ts +10 -0
  82. package/dist/types/slimmer/common/types.d.ts +30 -0
  83. package/dist/types/slimmer/decorators/inject-slimmer.d.ts +24 -0
  84. package/dist/types/slimmer/decorators/map-to-component-field.d.ts +16 -0
  85. package/dist/types/slimmer/decorators/map-to-component-method.d.ts +11 -0
  86. package/dist/types/slimmer/decorators/slimmer.d.ts +10 -0
  87. package/dist/types/slimmer/index.d.ts +6 -0
  88. package/dist/types/stencil-public-runtime.d.ts +1879 -0
  89. package/dist/types/utils/labels.d.ts +13 -0
  90. package/dist/types/utils/types.d.ts +106 -0
  91. package/dist/types/vega.d.ts +11 -0
  92. package/loader/cdn.js +1 -0
  93. package/loader/index.cjs.js +1 -0
  94. package/loader/index.d.ts +24 -0
  95. package/loader/index.es2017.js +1 -0
  96. package/loader/index.js +2 -0
  97. 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,4 @@
1
+ /** Identity helper giving a config literal its types without an annotation. */
2
+ export function createLoginConfig(config) {
3
+ return config;
4
+ }
@@ -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');
@@ -0,0 +1,2 @@
1
+ /** Narrows the `Object` a decorator is handed to something indexable. */
2
+ export const asBag = (target) => target;