@poly-x/core 0.1.1 → 0.2.0
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/dist/index.cjs +153 -2
- package/dist/index.d.cts +96 -2
- package/dist/index.d.mts +96 -2
- package/dist/index.mjs +147 -3
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -630,7 +630,7 @@ var SessionEngine = class {
|
|
|
630
630
|
this.applySignOut("expired", true);
|
|
631
631
|
return;
|
|
632
632
|
}
|
|
633
|
-
|
|
633
|
+
this.dispatch({ type: "refresh-failed-transient" });
|
|
634
634
|
}
|
|
635
635
|
async applySignOut(reason, broadcast) {
|
|
636
636
|
if (this.state.status === "signed-out") return;
|
|
@@ -789,6 +789,18 @@ function parseAuthorizeOutcome(status, body) {
|
|
|
789
789
|
if (statusField === "login_required" || b.code === "LOGIN_REQUIRED") return { type: "login_required" };
|
|
790
790
|
return { type: "login_required" };
|
|
791
791
|
}
|
|
792
|
+
/**
|
|
793
|
+
* Platform codes that mean "this app is misconfigured", verified against poly-auth
|
|
794
|
+
* `api/constants/auth-responses.ts` and `idp.controller.ts:validateClientAndRedirect`
|
|
795
|
+
* (2026-07-29). Only these map across — an unrecognised 400 keeps the previous
|
|
796
|
+
* behaviour rather than being mislabelled as misconfiguration.
|
|
797
|
+
*/
|
|
798
|
+
const CONFIGURATION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
799
|
+
"REDIRECT_URI_NOT_ALLOWED",
|
|
800
|
+
"INVALID_CLIENT",
|
|
801
|
+
"KEY_DEPLOYMENT_MISMATCH",
|
|
802
|
+
"KEY_ENVIRONMENT_MISMATCH"
|
|
803
|
+
]);
|
|
792
804
|
function parsePasswordOutcome(status, body) {
|
|
793
805
|
const b = asRecord$1(body);
|
|
794
806
|
const statusField = typeof b.status === "string" ? b.status : void 0;
|
|
@@ -809,6 +821,16 @@ function parsePasswordOutcome(status, body) {
|
|
|
809
821
|
ticket: typeof b.ticket === "string" ? b.ticket : void 0
|
|
810
822
|
};
|
|
811
823
|
if (status === 403) return { type: "no_access" };
|
|
824
|
+
if (statusField === "verification_required" && typeof b.challengeId === "string") return {
|
|
825
|
+
type: "verification_required",
|
|
826
|
+
challengeId: b.challengeId,
|
|
827
|
+
factorTypes: Array.isArray(b.factorTypes) ? b.factorTypes.filter((f) => typeof f === "string") : []
|
|
828
|
+
};
|
|
829
|
+
if (status === 400 && typeof b.code === "string" && CONFIGURATION_ERROR_CODES.has(b.code)) return {
|
|
830
|
+
type: "configuration_error",
|
|
831
|
+
code: b.code,
|
|
832
|
+
message: typeof b.message === "string" ? b.message : void 0
|
|
833
|
+
};
|
|
812
834
|
return { type: "invalid_credentials" };
|
|
813
835
|
}
|
|
814
836
|
function parseResetOutcome(status, body) {
|
|
@@ -863,6 +885,23 @@ function deriveDisplayName(user) {
|
|
|
863
885
|
return displayString([displayString(user.firstName, MAX_DISPLAY_NAME), displayString(user.lastName, MAX_DISPLAY_NAME)].filter(Boolean).join(" "), MAX_DISPLAY_NAME) ?? displayString(user.username, MAX_DISPLAY_NAME) ?? displayString(user.email, MAX_DISPLAY_NAME);
|
|
864
886
|
}
|
|
865
887
|
/**
|
|
888
|
+
* The bounded display subset of a user record: name, email, avatar (ADR-0006).
|
|
889
|
+
*
|
|
890
|
+
* Exported because a profile update has to re-derive exactly these — and only these — after the
|
|
891
|
+
* platform saves. `normalizeRefresh` carries claims forward unchanged, so without a deliberate
|
|
892
|
+
* re-derivation the display name and avatar are whatever they were at login, forever.
|
|
893
|
+
*
|
|
894
|
+
* Identity and authorization claims are deliberately absent: this returns what is safe to
|
|
895
|
+
* recompute from a profile response, not a whole claim set.
|
|
896
|
+
*/
|
|
897
|
+
function deriveDisplayClaims(user) {
|
|
898
|
+
return {
|
|
899
|
+
displayName: deriveDisplayName(user),
|
|
900
|
+
email: displayString(user.email, MAX_DISPLAY_NAME),
|
|
901
|
+
avatarUrl: displayString(user.profilePicture, MAX_AVATAR_URL)
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
866
905
|
* `POST /idp/token` → StoredSession. Claims carry identity plus the bounded display subset
|
|
867
906
|
* (ADR-0006); AUTHZ populates roles/permissions later.
|
|
868
907
|
*
|
|
@@ -1139,12 +1178,117 @@ var AuthClient = class {
|
|
|
1139
1178
|
}
|
|
1140
1179
|
};
|
|
1141
1180
|
//#endregion
|
|
1181
|
+
//#region src/profile/validation.ts
|
|
1182
|
+
/**
|
|
1183
|
+
* Profile input rules (FR-VALID-1…8).
|
|
1184
|
+
*
|
|
1185
|
+
* These are **mirrored from the platform's own validation** (`poly-auth`
|
|
1186
|
+
* `src/api/validations/common-fields.ts`), not invented here. That is the whole point: a value
|
|
1187
|
+
* this module accepts must not be one the platform rejects, or a person is shown an error they
|
|
1188
|
+
* cannot act on — and the error arrives after a round trip, attached to nothing in particular.
|
|
1189
|
+
*
|
|
1190
|
+
* The mirroring is the fragile part. It is pinned by tests that restate the platform's patterns
|
|
1191
|
+
* as literals and assert both agree, so a drift fails a build rather than a person's save.
|
|
1192
|
+
*/
|
|
1193
|
+
/**
|
|
1194
|
+
* One or more Unicode letters, optionally joined by a single space, hyphen or apostrophe —
|
|
1195
|
+
* "José", "O'Brien", "Anne-Marie", "van der Berg". No digits, no leading/trailing/double
|
|
1196
|
+
* separators. (poly-auth `PERSON_NAME_REGEX`.)
|
|
1197
|
+
*/
|
|
1198
|
+
const PERSON_NAME_REGEX = /^\p{L}+(?:[ '-]\p{L}+)*$/u;
|
|
1199
|
+
/** Leading "+", country code starting 1-9, then 7-14 more digits. (poly-auth `E164_PHONE_REGEX`.) */
|
|
1200
|
+
const E164_PHONE_REGEX = /^\+[1-9]\d{7,14}$/;
|
|
1201
|
+
/** poly-auth's person-name bounds for these two fields (`min: 1, max: 20`). */
|
|
1202
|
+
const NAME_MAX = 20;
|
|
1203
|
+
/** poly-auth's `imageUpload.middleware` cap. */
|
|
1204
|
+
const MAX_PROFILE_IMAGE_BYTES = 50 * 1024 * 1024;
|
|
1205
|
+
/** The text fields this release makes editable (FR-PROF-4 / D25). */
|
|
1206
|
+
const PROFILE_FIELDS = [
|
|
1207
|
+
"firstName",
|
|
1208
|
+
"lastName",
|
|
1209
|
+
"phoneNumber",
|
|
1210
|
+
"cnic",
|
|
1211
|
+
"gender"
|
|
1212
|
+
];
|
|
1213
|
+
function name(value, label) {
|
|
1214
|
+
const trimmed = (value ?? "").trim();
|
|
1215
|
+
if (trimmed === "") return `${label} is required`;
|
|
1216
|
+
if (trimmed.length > NAME_MAX) return `${label} cannot exceed ${NAME_MAX} characters`;
|
|
1217
|
+
if (!PERSON_NAME_REGEX.test(trimmed)) return `${label} may contain only letters, spaces, hyphens and apostrophes`;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Validate a whole draft and report **every** bad field, not the first one.
|
|
1221
|
+
*
|
|
1222
|
+
* Returning on the first failure would make someone fix one field per attempt — the experience
|
|
1223
|
+
* D27 exists to rule out.
|
|
1224
|
+
*/
|
|
1225
|
+
function validateProfileDraft(draft) {
|
|
1226
|
+
const errors = {};
|
|
1227
|
+
const firstName = name(draft.firstName, "First name");
|
|
1228
|
+
if (firstName) errors.firstName = firstName;
|
|
1229
|
+
const lastName = name(draft.lastName, "Last name");
|
|
1230
|
+
if (lastName) errors.lastName = lastName;
|
|
1231
|
+
const phone = (draft.phoneNumber ?? "").trim();
|
|
1232
|
+
if (phone !== "" && !E164_PHONE_REGEX.test(phone)) errors.phoneNumber = "Phone number must include the country code, e.g. +14155552671";
|
|
1233
|
+
const cnic = (draft.cnic ?? "").trim();
|
|
1234
|
+
if (cnic !== "" && cnic.replace(/\D/g, "") === "") errors.cnic = "National ID must contain digits";
|
|
1235
|
+
return errors;
|
|
1236
|
+
}
|
|
1237
|
+
/** `null` when the file is acceptable (or absent — a photo is optional). */
|
|
1238
|
+
function validateProfileImage(file) {
|
|
1239
|
+
if (!file) return null;
|
|
1240
|
+
if (!file.type.startsWith("image/")) return "Choose an image file";
|
|
1241
|
+
if (file.size > 52428800) return `Image must be smaller than ${Math.round(MAX_PROFILE_IMAGE_BYTES / (1024 * 1024))}MB`;
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* The changed fields only, trimmed.
|
|
1246
|
+
*
|
|
1247
|
+
* Two deliberate exclusions: unchanged fields (so an untouched save is a no-op, FR-PROF-7) and
|
|
1248
|
+
* fields the person *emptied*. The platform's update applies truthy values only, so an empty
|
|
1249
|
+
* one is ignored today — but sending it would mean a future backend that honours empties could
|
|
1250
|
+
* blank stored data on a save nobody intended as a deletion.
|
|
1251
|
+
*/
|
|
1252
|
+
function buildProfileUpdate(draft, stored) {
|
|
1253
|
+
const update = {};
|
|
1254
|
+
for (const field of PROFILE_FIELDS) {
|
|
1255
|
+
const next = (draft[field] ?? "").trim();
|
|
1256
|
+
const current = (stored[field] ?? "").trim();
|
|
1257
|
+
if (next === "" || next === current) continue;
|
|
1258
|
+
update[field] = next;
|
|
1259
|
+
}
|
|
1260
|
+
return update;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* poly-auth's fixed conflict messages, mapped to the field each is about.
|
|
1264
|
+
*
|
|
1265
|
+
* Mirrors `IDENTITY_CONFLICT_MESSAGES` in `poly-auth/src/api/controllers/identity-conflict.util.ts`.
|
|
1266
|
+
* The platform reports a uniqueness conflict as a 409 with one of a known set of messages and no
|
|
1267
|
+
* machine-readable field, so this is the only way to put the error where the person can act on it
|
|
1268
|
+
* (FR-VALID-6) — and matching a fixed enum of strings is a mirror, not a guess at prose.
|
|
1269
|
+
*
|
|
1270
|
+
* `username` and `email` are absent on purpose: neither is editable here (D25), so there is no
|
|
1271
|
+
* field to attach them to and the caller must show them generally rather than drop them.
|
|
1272
|
+
*/
|
|
1273
|
+
const CONFLICT_MESSAGES = [["an account with this phone number already exists.", "phoneNumber"], ["an account with this cnic already exists.", "cnic"]];
|
|
1274
|
+
/**
|
|
1275
|
+
* Which editable field a platform conflict message is about, or `null` when it is about something
|
|
1276
|
+
* this surface does not edit — or is a message we do not recognise, in which case the caller
|
|
1277
|
+
* should show it as a general error. A reworded message degrades to that, rather than breaking.
|
|
1278
|
+
*/
|
|
1279
|
+
function conflictField(message) {
|
|
1280
|
+
const normalized = (message ?? "").trim().toLowerCase();
|
|
1281
|
+
if (normalized === "") return null;
|
|
1282
|
+
return CONFLICT_MESSAGES.find(([text]) => text === normalized)?.[1] ?? null;
|
|
1283
|
+
}
|
|
1284
|
+
//#endregion
|
|
1142
1285
|
//#region src/index.ts
|
|
1143
1286
|
/**
|
|
1144
1287
|
* @poly-x/core — framework-agnostic guts of the PolyX SDK.
|
|
1145
1288
|
*/
|
|
1146
1289
|
const PACKAGE_NAME = "@poly-x/core";
|
|
1147
|
-
|
|
1290
|
+
/** The published version of this package, injected at build time. */
|
|
1291
|
+
const version = "0.2.0";
|
|
1148
1292
|
//#endregion
|
|
1149
1293
|
exports.AuthClient = AuthClient;
|
|
1150
1294
|
exports.DEFAULT_RETRY_POLICY = DEFAULT_RETRY_POLICY;
|
|
@@ -1157,8 +1301,10 @@ exports.InMemoryLock = InMemoryLock;
|
|
|
1157
1301
|
exports.InMemoryPkceStore = InMemoryPkceStore;
|
|
1158
1302
|
exports.InMemorySessionChannel = InMemorySessionChannel;
|
|
1159
1303
|
exports.InMemorySessionStore = InMemorySessionStore;
|
|
1304
|
+
exports.MAX_PROFILE_IMAGE_BYTES = MAX_PROFILE_IMAGE_BYTES;
|
|
1160
1305
|
exports.MockTransport = MockTransport;
|
|
1161
1306
|
exports.PACKAGE_NAME = PACKAGE_NAME;
|
|
1307
|
+
exports.PROFILE_FIELDS = PROFILE_FIELDS;
|
|
1162
1308
|
exports.PolyXConfigError = PolyXConfigError;
|
|
1163
1309
|
exports.PolyXError = PolyXError;
|
|
1164
1310
|
exports.RateLimitedError = RateLimitedError;
|
|
@@ -1169,9 +1315,12 @@ exports.SystemClock = SystemClock;
|
|
|
1169
1315
|
exports.UnauthenticatedError = UnauthenticatedError;
|
|
1170
1316
|
exports.UpstreamError = UpstreamError;
|
|
1171
1317
|
exports.ValidationError = ValidationError;
|
|
1318
|
+
exports.buildProfileUpdate = buildProfileUpdate;
|
|
1172
1319
|
exports.computeChallenge = computeChallenge;
|
|
1320
|
+
exports.conflictField = conflictField;
|
|
1173
1321
|
exports.createSessionEngine = createSessionEngine;
|
|
1174
1322
|
exports.decodeJwtExp = decodeJwtExp;
|
|
1323
|
+
exports.deriveDisplayClaims = deriveDisplayClaims;
|
|
1175
1324
|
exports.generatePkce = generatePkce;
|
|
1176
1325
|
exports.isTransientStatus = isTransientStatus;
|
|
1177
1326
|
exports.mapPlatformError = mapPlatformError;
|
|
@@ -1185,4 +1334,6 @@ exports.randomState = randomState;
|
|
|
1185
1334
|
exports.redactSensitive = redactSensitive;
|
|
1186
1335
|
exports.reduceSessionState = reduce;
|
|
1187
1336
|
exports.resolveConfig = resolveConfig;
|
|
1337
|
+
exports.validateProfileDraft = validateProfileDraft;
|
|
1338
|
+
exports.validateProfileImage = validateProfileImage;
|
|
1188
1339
|
exports.version = version;
|
package/dist/index.d.cts
CHANGED
|
@@ -490,6 +490,34 @@ type PasswordAuthOutcome = {
|
|
|
490
490
|
ticket?: string;
|
|
491
491
|
} | {
|
|
492
492
|
type: "no_access";
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* The credentials were accepted, but the platform wants a second proof of identity
|
|
496
|
+
* before issuing a session.
|
|
497
|
+
*
|
|
498
|
+
* RESERVED, NOT IMPLEMENTED. No deployment produces this today — multi-factor
|
|
499
|
+
* authentication is a platform release of its own. It is modelled now because the v1
|
|
500
|
+
* specification claimed the sign-in machine already accommodated a step like this and
|
|
501
|
+
* it did not: adding the variant later would be a breaking change across three
|
|
502
|
+
* packages, which is precisely the "flip, not a rebuild" promise that would have been
|
|
503
|
+
* broken. See the FSD (FR-STEP-1…4).
|
|
504
|
+
*/
|
|
505
|
+
| {
|
|
506
|
+
type: "verification_required";
|
|
507
|
+
challengeId: string;
|
|
508
|
+
factorTypes: readonly string[];
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* The APP is misconfigured — not the user's credentials. Reported for an
|
|
512
|
+
* unregistered `redirect_uri`, an unknown client, or a key bound to another
|
|
513
|
+
* deployment/environment. Kept separate because collapsing it into
|
|
514
|
+
* `invalid_credentials` tells a developer with a deployment problem that their
|
|
515
|
+
* password is wrong, which is exactly what consuming apps had to work around.
|
|
516
|
+
*/
|
|
517
|
+
| {
|
|
518
|
+
type: "configuration_error";
|
|
519
|
+
code: string;
|
|
520
|
+
message?: string;
|
|
493
521
|
};
|
|
494
522
|
declare function parsePasswordOutcome(status: number, body: unknown): PasswordAuthOutcome;
|
|
495
523
|
/**
|
|
@@ -503,6 +531,21 @@ declare function parseResetOutcome(status: number, body: unknown): PasswordReset
|
|
|
503
531
|
//#endregion
|
|
504
532
|
//#region src/auth/normalize.d.ts
|
|
505
533
|
declare function decodeJwtExp(token: string): number | null;
|
|
534
|
+
/**
|
|
535
|
+
* The bounded display subset of a user record: name, email, avatar (ADR-0006).
|
|
536
|
+
*
|
|
537
|
+
* Exported because a profile update has to re-derive exactly these — and only these — after the
|
|
538
|
+
* platform saves. `normalizeRefresh` carries claims forward unchanged, so without a deliberate
|
|
539
|
+
* re-derivation the display name and avatar are whatever they were at login, forever.
|
|
540
|
+
*
|
|
541
|
+
* Identity and authorization claims are deliberately absent: this returns what is safe to
|
|
542
|
+
* recompute from a profile response, not a whole claim set.
|
|
543
|
+
*/
|
|
544
|
+
declare function deriveDisplayClaims(user: Record<string, unknown>): {
|
|
545
|
+
displayName?: string;
|
|
546
|
+
email?: string;
|
|
547
|
+
avatarUrl?: string;
|
|
548
|
+
};
|
|
506
549
|
/**
|
|
507
550
|
* `POST /idp/token` → StoredSession. Claims carry identity plus the bounded display subset
|
|
508
551
|
* (ADR-0006); AUTHZ populates roles/permissions later.
|
|
@@ -681,11 +724,62 @@ declare class AuthClient {
|
|
|
681
724
|
private toError;
|
|
682
725
|
}
|
|
683
726
|
//#endregion
|
|
727
|
+
//#region src/profile/validation.d.ts
|
|
728
|
+
/**
|
|
729
|
+
* Profile input rules (FR-VALID-1…8).
|
|
730
|
+
*
|
|
731
|
+
* These are **mirrored from the platform's own validation** (`poly-auth`
|
|
732
|
+
* `src/api/validations/common-fields.ts`), not invented here. That is the whole point: a value
|
|
733
|
+
* this module accepts must not be one the platform rejects, or a person is shown an error they
|
|
734
|
+
* cannot act on — and the error arrives after a round trip, attached to nothing in particular.
|
|
735
|
+
*
|
|
736
|
+
* The mirroring is the fragile part. It is pinned by tests that restate the platform's patterns
|
|
737
|
+
* as literals and assert both agree, so a drift fails a build rather than a person's save.
|
|
738
|
+
*/
|
|
739
|
+
/** poly-auth's `imageUpload.middleware` cap. */
|
|
740
|
+
declare const MAX_PROFILE_IMAGE_BYTES: number;
|
|
741
|
+
/** The text fields this release makes editable (FR-PROF-4 / D25). */
|
|
742
|
+
declare const PROFILE_FIELDS: readonly ["firstName", "lastName", "phoneNumber", "cnic", "gender"];
|
|
743
|
+
type ProfileField = (typeof PROFILE_FIELDS)[number];
|
|
744
|
+
type ProfileDraft = Partial<Record<ProfileField, string>>;
|
|
745
|
+
type ProfileErrors = Partial<Record<ProfileField, string>>;
|
|
746
|
+
/** The subset of `File` these rules need, so this stays testable without a DOM. */
|
|
747
|
+
interface ProfileImageLike {
|
|
748
|
+
type: string;
|
|
749
|
+
size: number;
|
|
750
|
+
name?: string;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Validate a whole draft and report **every** bad field, not the first one.
|
|
754
|
+
*
|
|
755
|
+
* Returning on the first failure would make someone fix one field per attempt — the experience
|
|
756
|
+
* D27 exists to rule out.
|
|
757
|
+
*/
|
|
758
|
+
declare function validateProfileDraft(draft: ProfileDraft): ProfileErrors;
|
|
759
|
+
/** `null` when the file is acceptable (or absent — a photo is optional). */
|
|
760
|
+
declare function validateProfileImage(file: ProfileImageLike | null | undefined): string | null;
|
|
761
|
+
/**
|
|
762
|
+
* The changed fields only, trimmed.
|
|
763
|
+
*
|
|
764
|
+
* Two deliberate exclusions: unchanged fields (so an untouched save is a no-op, FR-PROF-7) and
|
|
765
|
+
* fields the person *emptied*. The platform's update applies truthy values only, so an empty
|
|
766
|
+
* one is ignored today — but sending it would mean a future backend that honours empties could
|
|
767
|
+
* blank stored data on a save nobody intended as a deletion.
|
|
768
|
+
*/
|
|
769
|
+
declare function buildProfileUpdate(draft: ProfileDraft, stored: ProfileDraft): ProfileDraft;
|
|
770
|
+
/**
|
|
771
|
+
* Which editable field a platform conflict message is about, or `null` when it is about something
|
|
772
|
+
* this surface does not edit — or is a message we do not recognise, in which case the caller
|
|
773
|
+
* should show it as a general error. A reworded message degrades to that, rather than breaking.
|
|
774
|
+
*/
|
|
775
|
+
declare function conflictField(message: string | undefined | null): ProfileField | null;
|
|
776
|
+
//#endregion
|
|
684
777
|
//#region src/index.d.ts
|
|
685
778
|
/**
|
|
686
779
|
* @poly-x/core — framework-agnostic guts of the PolyX SDK.
|
|
687
780
|
*/
|
|
688
781
|
declare const PACKAGE_NAME = "@poly-x/core";
|
|
689
|
-
|
|
782
|
+
/** The published version of this package, injected at build time. */
|
|
783
|
+
declare const version: string;
|
|
690
784
|
//#endregion
|
|
691
|
-
export { AuthClient, type AuthClientOptions, type AuthorizeOutcome, type AuthorizeWithPasswordParams, type BrandingTokens, type BuildAuthorizeUrlParams, type Clock, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, ENDPOINTS, type ExchangeCodeParams, type FetchBrandingParams, FetchTransport, type FieldError, ForbiddenError, type HttpMethod, INITIAL_STATE as INITIAL_SESSION_STATE, InMemoryLock, InMemoryPkceStore, InMemorySessionChannel, InMemorySessionStore, type KeyEnv, type KeyKind, type Lock, type MockRoute, MockTransport, PACKAGE_NAME, type ParsedKey, type PasswordAuthOutcome, type PasswordResetOutcome, type PkceEntry, type PkcePair, type PkceStore, type PlatformResponseLike, PolyXConfigError, PolyXError, type PolyXErrorOptions, RateLimitedError, type RefreshTokens, type ResolveConfigInput, type ResolvedConfig, type RetryPolicy, type RuntimeContext, type Session, type SessionChannel, type SessionChannelEvent, type SessionClaims, SessionEngine, type SessionEngineOptions, type SessionEvent, SessionExpiredError, SessionRevokedError, type SessionState, type SessionStore, type SetPasswordParams, type SignedOutReason, type StoredSession, SystemClock, type TenantOption, type TimerHandle, type TokenSet, type Transport, type TransportRequest, type TransportResponse, UnauthenticatedError, UpstreamError, ValidationError, computeChallenge, createSessionEngine, decodeJwtExp, generatePkce, isTransientStatus, mapPlatformError, normalizeExchange, normalizeRefresh, parseAuthorizeOutcome, parsePasswordOutcome, parsePolyXKey, parseResetOutcome, randomState, redactSensitive, reduce as reduceSessionState, resolveConfig, version };
|
|
785
|
+
export { AuthClient, type AuthClientOptions, type AuthorizeOutcome, type AuthorizeWithPasswordParams, type BrandingTokens, type BuildAuthorizeUrlParams, type Clock, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, ENDPOINTS, type ExchangeCodeParams, type FetchBrandingParams, FetchTransport, type FieldError, ForbiddenError, type HttpMethod, INITIAL_STATE as INITIAL_SESSION_STATE, InMemoryLock, InMemoryPkceStore, InMemorySessionChannel, InMemorySessionStore, type KeyEnv, type KeyKind, type Lock, MAX_PROFILE_IMAGE_BYTES, type MockRoute, MockTransport, PACKAGE_NAME, PROFILE_FIELDS, type ParsedKey, type PasswordAuthOutcome, type PasswordResetOutcome, type PkceEntry, type PkcePair, type PkceStore, type PlatformResponseLike, PolyXConfigError, PolyXError, type PolyXErrorOptions, type ProfileDraft, type ProfileErrors, type ProfileField, type ProfileImageLike, RateLimitedError, type RefreshTokens, type ResolveConfigInput, type ResolvedConfig, type RetryPolicy, type RuntimeContext, type Session, type SessionChannel, type SessionChannelEvent, type SessionClaims, SessionEngine, type SessionEngineOptions, type SessionEvent, SessionExpiredError, SessionRevokedError, type SessionState, type SessionStore, type SetPasswordParams, type SignedOutReason, type StoredSession, SystemClock, type TenantOption, type TimerHandle, type TokenSet, type Transport, type TransportRequest, type TransportResponse, UnauthenticatedError, UpstreamError, ValidationError, buildProfileUpdate, computeChallenge, conflictField, createSessionEngine, decodeJwtExp, deriveDisplayClaims, generatePkce, isTransientStatus, mapPlatformError, normalizeExchange, normalizeRefresh, parseAuthorizeOutcome, parsePasswordOutcome, parsePolyXKey, parseResetOutcome, randomState, redactSensitive, reduce as reduceSessionState, resolveConfig, validateProfileDraft, validateProfileImage, version };
|
package/dist/index.d.mts
CHANGED
|
@@ -490,6 +490,34 @@ type PasswordAuthOutcome = {
|
|
|
490
490
|
ticket?: string;
|
|
491
491
|
} | {
|
|
492
492
|
type: "no_access";
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* The credentials were accepted, but the platform wants a second proof of identity
|
|
496
|
+
* before issuing a session.
|
|
497
|
+
*
|
|
498
|
+
* RESERVED, NOT IMPLEMENTED. No deployment produces this today — multi-factor
|
|
499
|
+
* authentication is a platform release of its own. It is modelled now because the v1
|
|
500
|
+
* specification claimed the sign-in machine already accommodated a step like this and
|
|
501
|
+
* it did not: adding the variant later would be a breaking change across three
|
|
502
|
+
* packages, which is precisely the "flip, not a rebuild" promise that would have been
|
|
503
|
+
* broken. See the FSD (FR-STEP-1…4).
|
|
504
|
+
*/
|
|
505
|
+
| {
|
|
506
|
+
type: "verification_required";
|
|
507
|
+
challengeId: string;
|
|
508
|
+
factorTypes: readonly string[];
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* The APP is misconfigured — not the user's credentials. Reported for an
|
|
512
|
+
* unregistered `redirect_uri`, an unknown client, or a key bound to another
|
|
513
|
+
* deployment/environment. Kept separate because collapsing it into
|
|
514
|
+
* `invalid_credentials` tells a developer with a deployment problem that their
|
|
515
|
+
* password is wrong, which is exactly what consuming apps had to work around.
|
|
516
|
+
*/
|
|
517
|
+
| {
|
|
518
|
+
type: "configuration_error";
|
|
519
|
+
code: string;
|
|
520
|
+
message?: string;
|
|
493
521
|
};
|
|
494
522
|
declare function parsePasswordOutcome(status: number, body: unknown): PasswordAuthOutcome;
|
|
495
523
|
/**
|
|
@@ -503,6 +531,21 @@ declare function parseResetOutcome(status: number, body: unknown): PasswordReset
|
|
|
503
531
|
//#endregion
|
|
504
532
|
//#region src/auth/normalize.d.ts
|
|
505
533
|
declare function decodeJwtExp(token: string): number | null;
|
|
534
|
+
/**
|
|
535
|
+
* The bounded display subset of a user record: name, email, avatar (ADR-0006).
|
|
536
|
+
*
|
|
537
|
+
* Exported because a profile update has to re-derive exactly these — and only these — after the
|
|
538
|
+
* platform saves. `normalizeRefresh` carries claims forward unchanged, so without a deliberate
|
|
539
|
+
* re-derivation the display name and avatar are whatever they were at login, forever.
|
|
540
|
+
*
|
|
541
|
+
* Identity and authorization claims are deliberately absent: this returns what is safe to
|
|
542
|
+
* recompute from a profile response, not a whole claim set.
|
|
543
|
+
*/
|
|
544
|
+
declare function deriveDisplayClaims(user: Record<string, unknown>): {
|
|
545
|
+
displayName?: string;
|
|
546
|
+
email?: string;
|
|
547
|
+
avatarUrl?: string;
|
|
548
|
+
};
|
|
506
549
|
/**
|
|
507
550
|
* `POST /idp/token` → StoredSession. Claims carry identity plus the bounded display subset
|
|
508
551
|
* (ADR-0006); AUTHZ populates roles/permissions later.
|
|
@@ -681,11 +724,62 @@ declare class AuthClient {
|
|
|
681
724
|
private toError;
|
|
682
725
|
}
|
|
683
726
|
//#endregion
|
|
727
|
+
//#region src/profile/validation.d.ts
|
|
728
|
+
/**
|
|
729
|
+
* Profile input rules (FR-VALID-1…8).
|
|
730
|
+
*
|
|
731
|
+
* These are **mirrored from the platform's own validation** (`poly-auth`
|
|
732
|
+
* `src/api/validations/common-fields.ts`), not invented here. That is the whole point: a value
|
|
733
|
+
* this module accepts must not be one the platform rejects, or a person is shown an error they
|
|
734
|
+
* cannot act on — and the error arrives after a round trip, attached to nothing in particular.
|
|
735
|
+
*
|
|
736
|
+
* The mirroring is the fragile part. It is pinned by tests that restate the platform's patterns
|
|
737
|
+
* as literals and assert both agree, so a drift fails a build rather than a person's save.
|
|
738
|
+
*/
|
|
739
|
+
/** poly-auth's `imageUpload.middleware` cap. */
|
|
740
|
+
declare const MAX_PROFILE_IMAGE_BYTES: number;
|
|
741
|
+
/** The text fields this release makes editable (FR-PROF-4 / D25). */
|
|
742
|
+
declare const PROFILE_FIELDS: readonly ["firstName", "lastName", "phoneNumber", "cnic", "gender"];
|
|
743
|
+
type ProfileField = (typeof PROFILE_FIELDS)[number];
|
|
744
|
+
type ProfileDraft = Partial<Record<ProfileField, string>>;
|
|
745
|
+
type ProfileErrors = Partial<Record<ProfileField, string>>;
|
|
746
|
+
/** The subset of `File` these rules need, so this stays testable without a DOM. */
|
|
747
|
+
interface ProfileImageLike {
|
|
748
|
+
type: string;
|
|
749
|
+
size: number;
|
|
750
|
+
name?: string;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Validate a whole draft and report **every** bad field, not the first one.
|
|
754
|
+
*
|
|
755
|
+
* Returning on the first failure would make someone fix one field per attempt — the experience
|
|
756
|
+
* D27 exists to rule out.
|
|
757
|
+
*/
|
|
758
|
+
declare function validateProfileDraft(draft: ProfileDraft): ProfileErrors;
|
|
759
|
+
/** `null` when the file is acceptable (or absent — a photo is optional). */
|
|
760
|
+
declare function validateProfileImage(file: ProfileImageLike | null | undefined): string | null;
|
|
761
|
+
/**
|
|
762
|
+
* The changed fields only, trimmed.
|
|
763
|
+
*
|
|
764
|
+
* Two deliberate exclusions: unchanged fields (so an untouched save is a no-op, FR-PROF-7) and
|
|
765
|
+
* fields the person *emptied*. The platform's update applies truthy values only, so an empty
|
|
766
|
+
* one is ignored today — but sending it would mean a future backend that honours empties could
|
|
767
|
+
* blank stored data on a save nobody intended as a deletion.
|
|
768
|
+
*/
|
|
769
|
+
declare function buildProfileUpdate(draft: ProfileDraft, stored: ProfileDraft): ProfileDraft;
|
|
770
|
+
/**
|
|
771
|
+
* Which editable field a platform conflict message is about, or `null` when it is about something
|
|
772
|
+
* this surface does not edit — or is a message we do not recognise, in which case the caller
|
|
773
|
+
* should show it as a general error. A reworded message degrades to that, rather than breaking.
|
|
774
|
+
*/
|
|
775
|
+
declare function conflictField(message: string | undefined | null): ProfileField | null;
|
|
776
|
+
//#endregion
|
|
684
777
|
//#region src/index.d.ts
|
|
685
778
|
/**
|
|
686
779
|
* @poly-x/core — framework-agnostic guts of the PolyX SDK.
|
|
687
780
|
*/
|
|
688
781
|
declare const PACKAGE_NAME = "@poly-x/core";
|
|
689
|
-
|
|
782
|
+
/** The published version of this package, injected at build time. */
|
|
783
|
+
declare const version: string;
|
|
690
784
|
//#endregion
|
|
691
|
-
export { AuthClient, type AuthClientOptions, type AuthorizeOutcome, type AuthorizeWithPasswordParams, type BrandingTokens, type BuildAuthorizeUrlParams, type Clock, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, ENDPOINTS, type ExchangeCodeParams, type FetchBrandingParams, FetchTransport, type FieldError, ForbiddenError, type HttpMethod, INITIAL_STATE as INITIAL_SESSION_STATE, InMemoryLock, InMemoryPkceStore, InMemorySessionChannel, InMemorySessionStore, type KeyEnv, type KeyKind, type Lock, type MockRoute, MockTransport, PACKAGE_NAME, type ParsedKey, type PasswordAuthOutcome, type PasswordResetOutcome, type PkceEntry, type PkcePair, type PkceStore, type PlatformResponseLike, PolyXConfigError, PolyXError, type PolyXErrorOptions, RateLimitedError, type RefreshTokens, type ResolveConfigInput, type ResolvedConfig, type RetryPolicy, type RuntimeContext, type Session, type SessionChannel, type SessionChannelEvent, type SessionClaims, SessionEngine, type SessionEngineOptions, type SessionEvent, SessionExpiredError, SessionRevokedError, type SessionState, type SessionStore, type SetPasswordParams, type SignedOutReason, type StoredSession, SystemClock, type TenantOption, type TimerHandle, type TokenSet, type Transport, type TransportRequest, type TransportResponse, UnauthenticatedError, UpstreamError, ValidationError, computeChallenge, createSessionEngine, decodeJwtExp, generatePkce, isTransientStatus, mapPlatformError, normalizeExchange, normalizeRefresh, parseAuthorizeOutcome, parsePasswordOutcome, parsePolyXKey, parseResetOutcome, randomState, redactSensitive, reduce as reduceSessionState, resolveConfig, version };
|
|
785
|
+
export { AuthClient, type AuthClientOptions, type AuthorizeOutcome, type AuthorizeWithPasswordParams, type BrandingTokens, type BuildAuthorizeUrlParams, type Clock, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, ENDPOINTS, type ExchangeCodeParams, type FetchBrandingParams, FetchTransport, type FieldError, ForbiddenError, type HttpMethod, INITIAL_STATE as INITIAL_SESSION_STATE, InMemoryLock, InMemoryPkceStore, InMemorySessionChannel, InMemorySessionStore, type KeyEnv, type KeyKind, type Lock, MAX_PROFILE_IMAGE_BYTES, type MockRoute, MockTransport, PACKAGE_NAME, PROFILE_FIELDS, type ParsedKey, type PasswordAuthOutcome, type PasswordResetOutcome, type PkceEntry, type PkcePair, type PkceStore, type PlatformResponseLike, PolyXConfigError, PolyXError, type PolyXErrorOptions, type ProfileDraft, type ProfileErrors, type ProfileField, type ProfileImageLike, RateLimitedError, type RefreshTokens, type ResolveConfigInput, type ResolvedConfig, type RetryPolicy, type RuntimeContext, type Session, type SessionChannel, type SessionChannelEvent, type SessionClaims, SessionEngine, type SessionEngineOptions, type SessionEvent, SessionExpiredError, SessionRevokedError, type SessionState, type SessionStore, type SetPasswordParams, type SignedOutReason, type StoredSession, SystemClock, type TenantOption, type TimerHandle, type TokenSet, type Transport, type TransportRequest, type TransportResponse, UnauthenticatedError, UpstreamError, ValidationError, buildProfileUpdate, computeChallenge, conflictField, createSessionEngine, decodeJwtExp, deriveDisplayClaims, generatePkce, isTransientStatus, mapPlatformError, normalizeExchange, normalizeRefresh, parseAuthorizeOutcome, parsePasswordOutcome, parsePolyXKey, parseResetOutcome, randomState, redactSensitive, reduce as reduceSessionState, resolveConfig, validateProfileDraft, validateProfileImage, version };
|
package/dist/index.mjs
CHANGED
|
@@ -629,7 +629,7 @@ var SessionEngine = class {
|
|
|
629
629
|
this.applySignOut("expired", true);
|
|
630
630
|
return;
|
|
631
631
|
}
|
|
632
|
-
|
|
632
|
+
this.dispatch({ type: "refresh-failed-transient" });
|
|
633
633
|
}
|
|
634
634
|
async applySignOut(reason, broadcast) {
|
|
635
635
|
if (this.state.status === "signed-out") return;
|
|
@@ -788,6 +788,18 @@ function parseAuthorizeOutcome(status, body) {
|
|
|
788
788
|
if (statusField === "login_required" || b.code === "LOGIN_REQUIRED") return { type: "login_required" };
|
|
789
789
|
return { type: "login_required" };
|
|
790
790
|
}
|
|
791
|
+
/**
|
|
792
|
+
* Platform codes that mean "this app is misconfigured", verified against poly-auth
|
|
793
|
+
* `api/constants/auth-responses.ts` and `idp.controller.ts:validateClientAndRedirect`
|
|
794
|
+
* (2026-07-29). Only these map across — an unrecognised 400 keeps the previous
|
|
795
|
+
* behaviour rather than being mislabelled as misconfiguration.
|
|
796
|
+
*/
|
|
797
|
+
const CONFIGURATION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
798
|
+
"REDIRECT_URI_NOT_ALLOWED",
|
|
799
|
+
"INVALID_CLIENT",
|
|
800
|
+
"KEY_DEPLOYMENT_MISMATCH",
|
|
801
|
+
"KEY_ENVIRONMENT_MISMATCH"
|
|
802
|
+
]);
|
|
791
803
|
function parsePasswordOutcome(status, body) {
|
|
792
804
|
const b = asRecord$1(body);
|
|
793
805
|
const statusField = typeof b.status === "string" ? b.status : void 0;
|
|
@@ -808,6 +820,16 @@ function parsePasswordOutcome(status, body) {
|
|
|
808
820
|
ticket: typeof b.ticket === "string" ? b.ticket : void 0
|
|
809
821
|
};
|
|
810
822
|
if (status === 403) return { type: "no_access" };
|
|
823
|
+
if (statusField === "verification_required" && typeof b.challengeId === "string") return {
|
|
824
|
+
type: "verification_required",
|
|
825
|
+
challengeId: b.challengeId,
|
|
826
|
+
factorTypes: Array.isArray(b.factorTypes) ? b.factorTypes.filter((f) => typeof f === "string") : []
|
|
827
|
+
};
|
|
828
|
+
if (status === 400 && typeof b.code === "string" && CONFIGURATION_ERROR_CODES.has(b.code)) return {
|
|
829
|
+
type: "configuration_error",
|
|
830
|
+
code: b.code,
|
|
831
|
+
message: typeof b.message === "string" ? b.message : void 0
|
|
832
|
+
};
|
|
811
833
|
return { type: "invalid_credentials" };
|
|
812
834
|
}
|
|
813
835
|
function parseResetOutcome(status, body) {
|
|
@@ -862,6 +884,23 @@ function deriveDisplayName(user) {
|
|
|
862
884
|
return displayString([displayString(user.firstName, MAX_DISPLAY_NAME), displayString(user.lastName, MAX_DISPLAY_NAME)].filter(Boolean).join(" "), MAX_DISPLAY_NAME) ?? displayString(user.username, MAX_DISPLAY_NAME) ?? displayString(user.email, MAX_DISPLAY_NAME);
|
|
863
885
|
}
|
|
864
886
|
/**
|
|
887
|
+
* The bounded display subset of a user record: name, email, avatar (ADR-0006).
|
|
888
|
+
*
|
|
889
|
+
* Exported because a profile update has to re-derive exactly these — and only these — after the
|
|
890
|
+
* platform saves. `normalizeRefresh` carries claims forward unchanged, so without a deliberate
|
|
891
|
+
* re-derivation the display name and avatar are whatever they were at login, forever.
|
|
892
|
+
*
|
|
893
|
+
* Identity and authorization claims are deliberately absent: this returns what is safe to
|
|
894
|
+
* recompute from a profile response, not a whole claim set.
|
|
895
|
+
*/
|
|
896
|
+
function deriveDisplayClaims(user) {
|
|
897
|
+
return {
|
|
898
|
+
displayName: deriveDisplayName(user),
|
|
899
|
+
email: displayString(user.email, MAX_DISPLAY_NAME),
|
|
900
|
+
avatarUrl: displayString(user.profilePicture, MAX_AVATAR_URL)
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
865
904
|
* `POST /idp/token` → StoredSession. Claims carry identity plus the bounded display subset
|
|
866
905
|
* (ADR-0006); AUTHZ populates roles/permissions later.
|
|
867
906
|
*
|
|
@@ -1138,11 +1177,116 @@ var AuthClient = class {
|
|
|
1138
1177
|
}
|
|
1139
1178
|
};
|
|
1140
1179
|
//#endregion
|
|
1180
|
+
//#region src/profile/validation.ts
|
|
1181
|
+
/**
|
|
1182
|
+
* Profile input rules (FR-VALID-1…8).
|
|
1183
|
+
*
|
|
1184
|
+
* These are **mirrored from the platform's own validation** (`poly-auth`
|
|
1185
|
+
* `src/api/validations/common-fields.ts`), not invented here. That is the whole point: a value
|
|
1186
|
+
* this module accepts must not be one the platform rejects, or a person is shown an error they
|
|
1187
|
+
* cannot act on — and the error arrives after a round trip, attached to nothing in particular.
|
|
1188
|
+
*
|
|
1189
|
+
* The mirroring is the fragile part. It is pinned by tests that restate the platform's patterns
|
|
1190
|
+
* as literals and assert both agree, so a drift fails a build rather than a person's save.
|
|
1191
|
+
*/
|
|
1192
|
+
/**
|
|
1193
|
+
* One or more Unicode letters, optionally joined by a single space, hyphen or apostrophe —
|
|
1194
|
+
* "José", "O'Brien", "Anne-Marie", "van der Berg". No digits, no leading/trailing/double
|
|
1195
|
+
* separators. (poly-auth `PERSON_NAME_REGEX`.)
|
|
1196
|
+
*/
|
|
1197
|
+
const PERSON_NAME_REGEX = /^\p{L}+(?:[ '-]\p{L}+)*$/u;
|
|
1198
|
+
/** Leading "+", country code starting 1-9, then 7-14 more digits. (poly-auth `E164_PHONE_REGEX`.) */
|
|
1199
|
+
const E164_PHONE_REGEX = /^\+[1-9]\d{7,14}$/;
|
|
1200
|
+
/** poly-auth's person-name bounds for these two fields (`min: 1, max: 20`). */
|
|
1201
|
+
const NAME_MAX = 20;
|
|
1202
|
+
/** poly-auth's `imageUpload.middleware` cap. */
|
|
1203
|
+
const MAX_PROFILE_IMAGE_BYTES = 50 * 1024 * 1024;
|
|
1204
|
+
/** The text fields this release makes editable (FR-PROF-4 / D25). */
|
|
1205
|
+
const PROFILE_FIELDS = [
|
|
1206
|
+
"firstName",
|
|
1207
|
+
"lastName",
|
|
1208
|
+
"phoneNumber",
|
|
1209
|
+
"cnic",
|
|
1210
|
+
"gender"
|
|
1211
|
+
];
|
|
1212
|
+
function name(value, label) {
|
|
1213
|
+
const trimmed = (value ?? "").trim();
|
|
1214
|
+
if (trimmed === "") return `${label} is required`;
|
|
1215
|
+
if (trimmed.length > NAME_MAX) return `${label} cannot exceed ${NAME_MAX} characters`;
|
|
1216
|
+
if (!PERSON_NAME_REGEX.test(trimmed)) return `${label} may contain only letters, spaces, hyphens and apostrophes`;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Validate a whole draft and report **every** bad field, not the first one.
|
|
1220
|
+
*
|
|
1221
|
+
* Returning on the first failure would make someone fix one field per attempt — the experience
|
|
1222
|
+
* D27 exists to rule out.
|
|
1223
|
+
*/
|
|
1224
|
+
function validateProfileDraft(draft) {
|
|
1225
|
+
const errors = {};
|
|
1226
|
+
const firstName = name(draft.firstName, "First name");
|
|
1227
|
+
if (firstName) errors.firstName = firstName;
|
|
1228
|
+
const lastName = name(draft.lastName, "Last name");
|
|
1229
|
+
if (lastName) errors.lastName = lastName;
|
|
1230
|
+
const phone = (draft.phoneNumber ?? "").trim();
|
|
1231
|
+
if (phone !== "" && !E164_PHONE_REGEX.test(phone)) errors.phoneNumber = "Phone number must include the country code, e.g. +14155552671";
|
|
1232
|
+
const cnic = (draft.cnic ?? "").trim();
|
|
1233
|
+
if (cnic !== "" && cnic.replace(/\D/g, "") === "") errors.cnic = "National ID must contain digits";
|
|
1234
|
+
return errors;
|
|
1235
|
+
}
|
|
1236
|
+
/** `null` when the file is acceptable (or absent — a photo is optional). */
|
|
1237
|
+
function validateProfileImage(file) {
|
|
1238
|
+
if (!file) return null;
|
|
1239
|
+
if (!file.type.startsWith("image/")) return "Choose an image file";
|
|
1240
|
+
if (file.size > 52428800) return `Image must be smaller than ${Math.round(MAX_PROFILE_IMAGE_BYTES / (1024 * 1024))}MB`;
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* The changed fields only, trimmed.
|
|
1245
|
+
*
|
|
1246
|
+
* Two deliberate exclusions: unchanged fields (so an untouched save is a no-op, FR-PROF-7) and
|
|
1247
|
+
* fields the person *emptied*. The platform's update applies truthy values only, so an empty
|
|
1248
|
+
* one is ignored today — but sending it would mean a future backend that honours empties could
|
|
1249
|
+
* blank stored data on a save nobody intended as a deletion.
|
|
1250
|
+
*/
|
|
1251
|
+
function buildProfileUpdate(draft, stored) {
|
|
1252
|
+
const update = {};
|
|
1253
|
+
for (const field of PROFILE_FIELDS) {
|
|
1254
|
+
const next = (draft[field] ?? "").trim();
|
|
1255
|
+
const current = (stored[field] ?? "").trim();
|
|
1256
|
+
if (next === "" || next === current) continue;
|
|
1257
|
+
update[field] = next;
|
|
1258
|
+
}
|
|
1259
|
+
return update;
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* poly-auth's fixed conflict messages, mapped to the field each is about.
|
|
1263
|
+
*
|
|
1264
|
+
* Mirrors `IDENTITY_CONFLICT_MESSAGES` in `poly-auth/src/api/controllers/identity-conflict.util.ts`.
|
|
1265
|
+
* The platform reports a uniqueness conflict as a 409 with one of a known set of messages and no
|
|
1266
|
+
* machine-readable field, so this is the only way to put the error where the person can act on it
|
|
1267
|
+
* (FR-VALID-6) — and matching a fixed enum of strings is a mirror, not a guess at prose.
|
|
1268
|
+
*
|
|
1269
|
+
* `username` and `email` are absent on purpose: neither is editable here (D25), so there is no
|
|
1270
|
+
* field to attach them to and the caller must show them generally rather than drop them.
|
|
1271
|
+
*/
|
|
1272
|
+
const CONFLICT_MESSAGES = [["an account with this phone number already exists.", "phoneNumber"], ["an account with this cnic already exists.", "cnic"]];
|
|
1273
|
+
/**
|
|
1274
|
+
* Which editable field a platform conflict message is about, or `null` when it is about something
|
|
1275
|
+
* this surface does not edit — or is a message we do not recognise, in which case the caller
|
|
1276
|
+
* should show it as a general error. A reworded message degrades to that, rather than breaking.
|
|
1277
|
+
*/
|
|
1278
|
+
function conflictField(message) {
|
|
1279
|
+
const normalized = (message ?? "").trim().toLowerCase();
|
|
1280
|
+
if (normalized === "") return null;
|
|
1281
|
+
return CONFLICT_MESSAGES.find(([text]) => text === normalized)?.[1] ?? null;
|
|
1282
|
+
}
|
|
1283
|
+
//#endregion
|
|
1141
1284
|
//#region src/index.ts
|
|
1142
1285
|
/**
|
|
1143
1286
|
* @poly-x/core — framework-agnostic guts of the PolyX SDK.
|
|
1144
1287
|
*/
|
|
1145
1288
|
const PACKAGE_NAME = "@poly-x/core";
|
|
1146
|
-
|
|
1289
|
+
/** The published version of this package, injected at build time. */
|
|
1290
|
+
const version = "0.2.0";
|
|
1147
1291
|
//#endregion
|
|
1148
|
-
export { AuthClient, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, ENDPOINTS, FetchTransport, ForbiddenError, INITIAL_STATE as INITIAL_SESSION_STATE, InMemoryLock, InMemoryPkceStore, InMemorySessionChannel, InMemorySessionStore, MockTransport, PACKAGE_NAME, PolyXConfigError, PolyXError, RateLimitedError, SessionEngine, SessionExpiredError, SessionRevokedError, SystemClock, UnauthenticatedError, UpstreamError, ValidationError, computeChallenge, createSessionEngine, decodeJwtExp, generatePkce, isTransientStatus, mapPlatformError, normalizeExchange, normalizeRefresh, parseAuthorizeOutcome, parsePasswordOutcome, parsePolyXKey, parseResetOutcome, randomState, redactSensitive, reduce as reduceSessionState, resolveConfig, version };
|
|
1292
|
+
export { AuthClient, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_MS, ENDPOINTS, FetchTransport, ForbiddenError, INITIAL_STATE as INITIAL_SESSION_STATE, InMemoryLock, InMemoryPkceStore, InMemorySessionChannel, InMemorySessionStore, MAX_PROFILE_IMAGE_BYTES, MockTransport, PACKAGE_NAME, PROFILE_FIELDS, PolyXConfigError, PolyXError, RateLimitedError, SessionEngine, SessionExpiredError, SessionRevokedError, SystemClock, UnauthenticatedError, UpstreamError, ValidationError, buildProfileUpdate, computeChallenge, conflictField, createSessionEngine, decodeJwtExp, deriveDisplayClaims, generatePkce, isTransientStatus, mapPlatformError, normalizeExchange, normalizeRefresh, parseAuthorizeOutcome, parsePasswordOutcome, parsePolyXKey, parseResetOutcome, randomState, redactSensitive, reduce as reduceSessionState, resolveConfig, validateProfileDraft, validateProfileImage, version };
|