From cfe59daed85c51d58ed2c4998410b3e057998992 Mon Sep 17 00:00:00 2001
From: masnwilliams <43387599+masnwilliams@users.noreply.github.com>
Date: Sat, 1 Aug 2026 15:52:50 +0000
Subject: [PATCH 1/2] Show notice for rejected fields
---
.changeset/replace-existing-field-notice.md | 5 ++
.../src/appearance/types.ts | 2 +
.../src/components/UnifiedAuthForm.tsx | 5 ++
packages/managed-auth-react/src/lib/types.ts | 2 +
.../src/localization/defaults.ts | 2 +
.../src/localization/types.ts | 1 +
.../src/session/state.test.ts | 61 +++++++++++++++++++
.../managed-auth-react/src/session/state.ts | 28 ++++++---
.../managed-auth-react/src/styles/styles.css | 6 ++
9 files changed, 102 insertions(+), 10 deletions(-)
create mode 100644 .changeset/replace-existing-field-notice.md
diff --git a/.changeset/replace-existing-field-notice.md b/.changeset/replace-existing-field-notice.md
new file mode 100644
index 0000000..60f1153
--- /dev/null
+++ b/.changeset/replace-existing-field-notice.md
@@ -0,0 +1,5 @@
+---
+"@onkernel/managed-auth-react": minor
+---
+
+Show a notice when a canonical field has `reason: "rejected"`. Adds the `fieldRejectedNotice` label and the `inputRejectedNotice` appearance slot.
diff --git a/packages/managed-auth-react/src/appearance/types.ts b/packages/managed-auth-react/src/appearance/types.ts
index 16eb621..4db0796 100644
--- a/packages/managed-auth-react/src/appearance/types.ts
+++ b/packages/managed-auth-react/src/appearance/types.ts
@@ -71,6 +71,8 @@ export interface AppearanceElements {
input?: ElementValue;
/** Small hint text under an input. */
inputHint?: ElementValue;
+ /** Notice under an input whose saved value was rejected. */
+ inputRejectedNotice?: ElementValue;
/** Password show/hide toggle button. */
passwordToggle?: ElementValue;
/** All buttons — base. */
diff --git a/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx b/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx
index 49395f0..0306f7f 100644
--- a/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx
+++ b/packages/managed-auth-react/src/components/UnifiedAuthForm.tsx
@@ -300,6 +300,11 @@ export function UnifiedAuthForm({
}
/>
)}
+ {field.reason === "rejected" && (
+
+ {l.fieldRejectedNotice}
+
+ )}
{field.hint && (
{field.hint}
)}
diff --git a/packages/managed-auth-react/src/lib/types.ts b/packages/managed-auth-react/src/lib/types.ts
index c78e79d..681d1ec 100644
--- a/packages/managed-auth-react/src/lib/types.ts
+++ b/packages/managed-auth-react/src/lib/types.ts
@@ -35,6 +35,7 @@ export interface DiscoveredField {
type: "text" | "email" | "password" | "tel" | "code" | "totp";
placeholder?: string;
required?: boolean;
+ reason?: "missing" | "rejected";
hint?: string;
linked_mfa_type?: MFAType;
}
@@ -73,6 +74,7 @@ export interface ManagedAuthField {
| "text";
label?: string;
required?: boolean;
+ reason: "missing" | "rejected";
hint?: string;
observed_selector?: string | null;
}
diff --git a/packages/managed-auth-react/src/localization/defaults.ts b/packages/managed-auth-react/src/localization/defaults.ts
index 01ccebc..9dddebc 100644
--- a/packages/managed-auth-react/src/localization/defaults.ts
+++ b/packages/managed-auth-react/src/localization/defaults.ts
@@ -42,6 +42,8 @@ export const DEFAULT_LOCALIZATION: Localizer = {
orDivider: "or",
passwordShow: "Show password",
passwordHide: "Hide password",
+ fieldRejectedNotice:
+ "The saved value was rejected. Enter a new one to continue.",
credentialSafetyNotice:
"Your credentials are encrypted and sent directly from your browser. They are never shared with anyone or any LLM.",
mfaTypeLabels: {
diff --git a/packages/managed-auth-react/src/localization/types.ts b/packages/managed-auth-react/src/localization/types.ts
index 1c01c71..73d28a7 100644
--- a/packages/managed-auth-react/src/localization/types.ts
+++ b/packages/managed-auth-react/src/localization/types.ts
@@ -39,6 +39,7 @@ export interface Localization {
orDivider?: string;
passwordShow?: string;
passwordHide?: string;
+ fieldRejectedNotice?: string;
credentialSafetyNotice?: string;
/** MFA type labels. */
mfaTypeLabels?: Partial>;
diff --git a/packages/managed-auth-react/src/session/state.test.ts b/packages/managed-auth-react/src/session/state.test.ts
index 338f3fa..a05e717 100644
--- a/packages/managed-auth-react/src/session/state.test.ts
+++ b/packages/managed-auth-react/src/session/state.test.ts
@@ -28,6 +28,7 @@ describe("mergeStateEvent", () => {
ref: "email",
type: "identifier",
label: "Email",
+ reason: "missing",
},
],
choices: [{ id: "google", type: "sso_provider", label: "Google" }],
@@ -195,18 +196,21 @@ describe("normalizeManagedAuthState", () => {
ref: "username",
type: "identifier",
label: "Username",
+ reason: "missing",
},
{
id: "field_email",
ref: "email",
type: "identifier",
label: "Email",
+ reason: "missing",
},
{
id: "field_phone",
ref: "phone_number",
type: "identifier",
label: "Phone",
+ reason: "missing",
},
],
choices: [
@@ -259,3 +263,60 @@ describe("normalizeManagedAuthState", () => {
});
});
});
+
+describe("field reason", () => {
+ test("carries the canonical field reason into the rendered field", () => {
+ const state = managedAuthState({
+ fields: [
+ {
+ id: "field_password",
+ ref: "password",
+ type: "password",
+ label: "Password",
+ reason: "rejected",
+ },
+ ],
+ });
+
+ const derived = normalizeManagedAuthState(state).discovered_fields;
+ expect(derived?.[0].reason).toBe("rejected");
+ });
+
+ test("projects the exact field shape the form renders", () => {
+ const state = managedAuthState({
+ fields: [
+ {
+ id: "field_password",
+ ref: "password",
+ type: "password",
+ label: "Password",
+ required: false,
+ reason: "missing",
+ },
+ ],
+ discovered_fields: [
+ {
+ name: "password",
+ type: "password",
+ label: "Password",
+ placeholder: "Enter password",
+ hint: "Use the password for this account",
+ },
+ ],
+ });
+
+ expect(normalizeManagedAuthState(state).discovered_fields).toStrictEqual([
+ {
+ id: "field_password",
+ ref: "password",
+ name: "field_password",
+ type: "password",
+ label: "Password",
+ placeholder: "Enter password",
+ required: false,
+ reason: "missing",
+ hint: "Use the password for this account",
+ },
+ ]);
+ });
+});
diff --git a/packages/managed-auth-react/src/session/state.ts b/packages/managed-auth-react/src/session/state.ts
index 43e3501..19f4e77 100644
--- a/packages/managed-auth-react/src/session/state.ts
+++ b/packages/managed-auth-react/src/session/state.ts
@@ -57,17 +57,25 @@ function fieldTypeToDiscoveredType(
function fieldsFromCanonical(
fields: ManagedAuthField[],
+ legacyFields: DiscoveredField[] | null | undefined,
): DiscoveredField[] | null {
if (!fields.length) return null;
- return fields.map((field) => ({
- id: field.id,
- ref: field.ref,
- name: field.id,
- type: fieldTypeToDiscoveredType(field),
- label: field.label || field.ref,
- required: field.required ?? true,
- hint: field.hint,
- }));
+ return fields.map((field) => {
+ const legacyField = legacyFields?.find(
+ (candidate) => candidate.name === field.ref,
+ );
+ return {
+ id: field.id,
+ ref: field.ref,
+ name: field.id,
+ type: fieldTypeToDiscoveredType(field),
+ label: field.label || field.ref,
+ placeholder: legacyField?.placeholder,
+ required: field.required ?? true,
+ reason: field.reason,
+ hint: field.hint ?? legacyField?.hint,
+ };
+ });
}
function ssoButtonsFromCanonical(
@@ -177,7 +185,7 @@ export function normalizeManagedAuthState(
return {
...state,
discovered_fields: hasCanonicalFields
- ? fieldsFromCanonical(state.fields ?? [])
+ ? fieldsFromCanonical(state.fields ?? [], state.discovered_fields)
: state.discovered_fields,
pending_sso_buttons: hasCanonicalChoices
? ssoButtonsFromCanonical(state.choices ?? [])
diff --git a/packages/managed-auth-react/src/styles/styles.css b/packages/managed-auth-react/src/styles/styles.css
index be0412e..38203cf 100644
--- a/packages/managed-auth-react/src/styles/styles.css
+++ b/packages/managed-auth-react/src/styles/styles.css
@@ -604,6 +604,12 @@
color: var(--kma-color-muted-foreground);
}
+.kma-input-rejected-notice {
+ margin: 0;
+ font-size: var(--kma-font-size-sm);
+ color: var(--kma-color-danger);
+}
+
.kma-password-toggle {
position: absolute;
right: 10px;
From bd66c2888e41a85354eba43a9916fa3b298db36b Mon Sep 17 00:00:00 2001
From: masnwilliams <43387599+masnwilliams@users.noreply.github.com>
Date: Thu, 20 Aug 2026 20:05:58 +0000
Subject: [PATCH 2/2] Validate protocol against latest SDK
---
bun.lock | 3 ++
package.json | 1 +
packages/managed-auth-react/README.md | 4 +-
.../src/lib/sdk-contract.test.ts | 39 +++++++++++++++++++
packages/managed-auth-react/src/lib/types.ts | 2 +-
.../src/session/useManagedAuthSession.test.ts | 1 +
6 files changed, 47 insertions(+), 3 deletions(-)
create mode 100644 packages/managed-auth-react/src/lib/sdk-contract.test.ts
diff --git a/bun.lock b/bun.lock
index 3cfc67f..d5ab46c 100644
--- a/bun.lock
+++ b/bun.lock
@@ -7,6 +7,7 @@
"devDependencies": {
"@changesets/changelog-github": "0.7.0",
"@changesets/cli": "^2.27.0",
+ "@onkernel/sdk": "0.93.0",
"@types/bun": "1.2.21",
"@types/node": "^20",
"@types/react-test-renderer": "18.3.1",
@@ -208,6 +209,8 @@
"@onkernel/managed-auth-react-demo": ["@onkernel/managed-auth-react-demo@workspace:packages/demo"],
+ "@onkernel/sdk": ["@onkernel/sdk@0.93.0", "", {}, "sha512-Hb28fq/XGoDbJGOZXHX7MPdFbBs9OtVER44Xo1p6VT4123xpQMVHCGdR4AdW2KuEEwJpXd1yxuTn0GPvUG8dXw=="],
+
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="],
diff --git a/package.json b/package.json
index 12947d0..405b379 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"devDependencies": {
"@changesets/changelog-github": "0.7.0",
"@changesets/cli": "^2.27.0",
+ "@onkernel/sdk": "0.93.0",
"@types/bun": "1.2.21",
"@types/node": "^20",
"@types/react-test-renderer": "18.3.1",
diff --git a/packages/managed-auth-react/README.md b/packages/managed-auth-react/README.md
index a1750c2..aca3144 100644
--- a/packages/managed-auth-react/README.md
+++ b/packages/managed-auth-react/README.md
@@ -3,8 +3,8 @@
One drop-in React component for [Kernel](https://onkernel.com) managed auth. Plug in your session, customize every element, ship.
```bash
-bun add @onkernel/managed-auth-react
-# or: npm install @onkernel/managed-auth-react
+bun add @onkernel/managed-auth-react @onkernel/sdk@latest
+# or: npm install @onkernel/managed-auth-react @onkernel/sdk@latest
```
## Quick start
diff --git a/packages/managed-auth-react/src/lib/sdk-contract.test.ts b/packages/managed-auth-react/src/lib/sdk-contract.test.ts
new file mode 100644
index 0000000..fe74208
--- /dev/null
+++ b/packages/managed-auth-react/src/lib/sdk-contract.test.ts
@@ -0,0 +1,39 @@
+import { expect, test } from "bun:test";
+import type {
+ ConnectionFollowResponse,
+ ConnectionSubmitParams,
+ ManagedAuth as SDKManagedAuth,
+} from "@onkernel/sdk/resources/auth/connections";
+import type { ManagedAuthSubmitBody } from "./api";
+import type { ManagedAuthChoice, ManagedAuthField } from "./types";
+
+type Equal = [Left] extends [Right]
+ ? [Right] extends [Left]
+ ? true
+ : false
+ : false;
+
+test("canonical protocol types match @onkernel/sdk", () => {
+ type SDKStateEvent = Extract<
+ ConnectionFollowResponse,
+ { event: "managed_auth_state" }
+ >;
+ type SDKEventField = NonNullable[number];
+ type SDKEventChoice = NonNullable[number];
+
+ const fieldMatches: Equal = true;
+ const choiceMatches: Equal = true;
+ const eventFieldMatches: Equal = true;
+ const eventChoiceMatches: Equal = true;
+ const submitMatches: ManagedAuthSubmitBody extends ConnectionSubmitParams
+ ? true
+ : false = true;
+
+ expect([
+ fieldMatches,
+ choiceMatches,
+ eventFieldMatches,
+ eventChoiceMatches,
+ submitMatches,
+ ]).toEqual([true, true, true, true, true]);
+});
diff --git a/packages/managed-auth-react/src/lib/types.ts b/packages/managed-auth-react/src/lib/types.ts
index 681d1ec..01fb154 100644
--- a/packages/managed-auth-react/src/lib/types.ts
+++ b/packages/managed-auth-react/src/lib/types.ts
@@ -1,5 +1,5 @@
// Protocol types for Kernel managed auth.
-// Vendored from @onkernel/sdk to keep this package runtime-free.
+// Synchronized with @onkernel/sdk v0.93.0 and vendored to keep this package runtime-free.
export type FlowStatus =
| "PENDING"
diff --git a/packages/managed-auth-react/src/session/useManagedAuthSession.test.ts b/packages/managed-auth-react/src/session/useManagedAuthSession.test.ts
index a5821de..def4b85 100644
--- a/packages/managed-auth-react/src/session/useManagedAuthSession.test.ts
+++ b/packages/managed-auth-react/src/session/useManagedAuthSession.test.ts
@@ -28,6 +28,7 @@ function awaitingInputState(): ManagedAuthResponse {
ref: "email",
type: "identifier",
label: "Email",
+ reason: "missing",
},
],
choices: [],