@astram/guard 0.11.1 → 0.11.2
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/auth/ports/email-address-changer.d.ts +3 -1
- package/dist/auth/ports/email-address-changer.d.ts.map +1 -1
- package/dist/auth/ports/profile-writer.d.ts +5 -1
- package/dist/auth/ports/profile-writer.d.ts.map +1 -1
- package/dist/auth/tokens.d.ts +2 -1
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +2 -1
- package/dist/authentication/mariadb-email-address-changer.d.ts +10 -3
- package/dist/authentication/mariadb-email-address-changer.d.ts.map +1 -1
- package/dist/authentication/mariadb-email-address-changer.js +27 -9
- package/dist/authentication/mariadb-profile-writer.d.ts +2 -1
- package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-writer.js +6 -4
- package/package.json +1 -1
|
@@ -58,7 +58,9 @@ export interface EmailAddressChanger {
|
|
|
58
58
|
* refuse a collision (another user's `u_email`, another principal's `ic_identity_hint`;
|
|
59
59
|
* collation-equal, locked reads, self excluded), write `u_email`, move `ic_identity_hint` when it
|
|
60
60
|
* equalled the OLD email (a username hint is untouched), and burn the user's pending
|
|
61
|
-
* email-change tokens.
|
|
61
|
+
* email-change tokens. A unique-index refusal of the write (`uq_users_email` or the hint PK:
|
|
62
|
+
* a row written meanwhile by a path that skipped the collision read) is `unavailable` too.
|
|
63
|
+
* Throws on any other store error (caller → 503).
|
|
62
64
|
*/
|
|
63
65
|
change(request: EmailChangeRequest): Promise<EmailChangeOutcome>;
|
|
64
66
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email-address-changer.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/email-address-changer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,+DAA+D;AAC/D,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,qFAAqF;IACrF,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAED,4EAA4E;AAC5E,MAAM,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAErF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,mFAAmF;IACnF,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAC3B,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;KAAE,KACtE,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAAE;AAC/F,2FAA2F;GACzF;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GAClC;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAChC;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAEjC,MAAM,WAAW,mBAAmB;IAClC,kGAAkG;IAClG,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAClF
|
|
1
|
+
{"version":3,"file":"email-address-changer.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/email-address-changer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,+DAA+D;AAC/D,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,qFAAqF;IACrF,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAED,4EAA4E;AAC5E,MAAM,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAErF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,mFAAmF;IACnF,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAC3B,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;KAAE,KACtE,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAAE;AAC/F,2FAA2F;GACzF;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GAClC;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAChC;IAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAEjC,MAAM,WAAW,mBAAmB;IAClC,kGAAkG;IAClG,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAClF;;;;;;;;OAQG;IACH,MAAM,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAClE"}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { CurrentUserProfile } from '../profile-resolver.js';
|
|
2
2
|
/**
|
|
3
|
-
* ProfileWriter — self-service profile edit
|
|
3
|
+
* ProfileWriter — self-service profile edit. Returns the refreshed
|
|
4
4
|
* display profile (names, never UUIDs). Consumer-plugged.
|
|
5
|
+
*
|
|
6
|
+
* Since Phase 12 (KI #14) `PATCH /account/profile` passes only `name`: an email change is
|
|
7
|
+
* requested, confirmed and written through the verified `EmailAddressChanger` flow. `email`
|
|
8
|
+
* stays in the type for compatibility; the framework default refuses it.
|
|
5
9
|
*/
|
|
6
10
|
export interface ProfileWriter {
|
|
7
11
|
update(principalId: string, changes: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile-writer.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/profile-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE
|
|
1
|
+
{"version":3,"file":"profile-writer.d.ts","sourceRoot":"","sources":["../../../src/auth/ports/profile-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,CACJ,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACzC,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAChC"}
|
package/dist/auth/tokens.d.ts
CHANGED
|
@@ -132,7 +132,8 @@ export declare const SHIELD_EMAIL_SENDER: unique symbol;
|
|
|
132
132
|
export declare const SHIELD_RECOVERY_CODE_STORE: unique symbol;
|
|
133
133
|
/**
|
|
134
134
|
* The `ProfileWriter` (guard port, `../auth/ports/profile-writer.ts`) — the
|
|
135
|
-
* self-service profile-edit seam (name
|
|
135
|
+
* self-service profile-edit seam (name; an email goes through the verified email-change flow).
|
|
136
|
+
* Consumed by `PATCH /account/profile`.
|
|
136
137
|
*/
|
|
137
138
|
export declare const SHIELD_PROFILE_WRITER: unique symbol;
|
|
138
139
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+CAA+C,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,YAAY,eAAyB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAW3E;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,eAA4C,CAAC;AAEzF;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,eAAyC,CAAC;AAEnF;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,eAA8C,CAAC;AAE7F;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,eAAwC,CAAC;AAEjF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,eAAyC,CAAC;AAEnF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,eAAgC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,+CAA+C,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,eAA2B,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,YAAY,eAAyB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAW3E;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,eAAqC,CAAC;AAE3E;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,eAA4C,CAAC;AAEzF;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,eAAyC,CAAC;AAEnF;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,eAA8C,CAAC;AAE7F;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,eAAwC,CAAC;AAEjF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,eAAyC,CAAC;AAEnF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,eAAgC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,eAA8B,CAAC;AAE7D;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,eAAmC,CAAC;AAEvE;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,6BAA6B,eAA0C,CAAC;AAErF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe,eAA4B,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,eAA2C,CAAC;AAEvF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,eAA0C,CAAC;AAErF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,eAAsC,CAAC;AAU7E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,eAA+B,CAAC;AAE/D;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,eAA+B,CAAC;AAE/D;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,eAAkC,CAAC;AAErE;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,eAAiC,CAAC;AAEnE;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,eAAoC,CAAC;AAEzE;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,eAA4C,CAAC;AAEzF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,eAAwC,CAAC"}
|
package/dist/auth/tokens.js
CHANGED
|
@@ -144,7 +144,8 @@ exports.SHIELD_EMAIL_SENDER = Symbol('SHIELD_EMAIL_SENDER');
|
|
|
144
144
|
exports.SHIELD_RECOVERY_CODE_STORE = Symbol('SHIELD_RECOVERY_CODE_STORE');
|
|
145
145
|
/**
|
|
146
146
|
* The `ProfileWriter` (guard port, `../auth/ports/profile-writer.ts`) — the
|
|
147
|
-
* self-service profile-edit seam (name
|
|
147
|
+
* self-service profile-edit seam (name; an email goes through the verified email-change flow).
|
|
148
|
+
* Consumed by `PATCH /account/profile`.
|
|
148
149
|
*/
|
|
149
150
|
exports.SHIELD_PROFILE_WRITER = Symbol('SHIELD_PROFILE_WRITER');
|
|
150
151
|
/**
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* paths share one collision rule and one login-hint rule.
|
|
7
7
|
*
|
|
8
8
|
* `change` (one MariaDB transaction, lock-then-recheck):
|
|
9
|
-
* 0. `GET_LOCK('shield_email_change')` — serialises email changes.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* 0. `GET_LOCK('shield_email_change')` — serialises email changes. The locked collision reads
|
|
10
|
+
* below look up a value that is normally ABSENT; under REPEATABLE READ that takes a gap lock
|
|
11
|
+
* on `uq_users_email` (migration 021, KI #87) and on the `ic_identity_hint` PK. Gap locks do
|
|
12
|
+
* not conflict with each other but block the other side's insert into the gap, so two
|
|
13
|
+
* concurrent changes into the same gap would deadlock (1213); the named lock prevents that.
|
|
12
14
|
* 1. lock the user row (`FOR UPDATE`); refuse `not_found`, `inactive` (when required), `stale`
|
|
13
15
|
* (stored email ≠ the expected old one, exact compare — A3);
|
|
14
16
|
* 2. refuse `unavailable` when another user's `u_email` or another principal's login hint is
|
|
@@ -18,6 +20,11 @@
|
|
|
18
20
|
* 4. login-hint follow: `ic_identity_hint` (the PK) is updated IN PLACE to the new address when it
|
|
19
21
|
* equalled the OLD email (collation-equal); a username hint matches nothing and stays;
|
|
20
22
|
* 5. burn the user's pending email-change tokens; run `withinTransaction`; commit.
|
|
23
|
+
* The unique index is the last word for writers that do not take the named lock (a direct insert,
|
|
24
|
+
* a user create): a duplicate key (1062) on step 3 or 4 is the same answer as step 2,
|
|
25
|
+
* `unavailable`, rolled back. The driver's message names the full address, so it is dropped here
|
|
26
|
+
* and never logged. Any other store error (a 1213 deadlock, a lock-wait timeout) is thrown
|
|
27
|
+
* (caller → 503).
|
|
21
28
|
* Every query is parameterized; addresses are compared by the column collation in SQL.
|
|
22
29
|
*/
|
|
23
30
|
import type { Pool } from 'mariadb';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mariadb-email-address-changer.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-email-address-changer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mariadb-email-address-changer.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-email-address-changer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,IAAI,EAAkB,MAAM,SAAS,CAAC;AACpD,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,wCAAwC,CAAC;AAqBhD,qBAAa,0BAA2B,YAAW,mBAAmB;IAElE,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBADH,IAAI,EAAE,IAAI,EACV,GAAG,GAAE,MAAM,MAAiB;IAGzC,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAejF,MAAM,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;YA6BxD,YAAY;CAqE3B"}
|
|
@@ -3,6 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MariaDbEmailAddressChanger = void 0;
|
|
4
4
|
/** Seconds to wait for the email-change named lock before giving up (caller → 503). */
|
|
5
5
|
const LOCK_TIMEOUT_S = 10;
|
|
6
|
+
/** MariaDB `ER_DUP_ENTRY`: a unique index (here `uq_users_email` or the hint PK) refused a write. */
|
|
7
|
+
const ER_DUP_ENTRY = 1062;
|
|
8
|
+
/** A refusal by a unique index, recognised by errno/code only (the message holds the value). */
|
|
9
|
+
function isDuplicateKey(err) {
|
|
10
|
+
if (typeof err !== 'object' || err === null)
|
|
11
|
+
return false;
|
|
12
|
+
const e = err;
|
|
13
|
+
return e.errno === ER_DUP_ENTRY || e.code === 'ER_DUP_ENTRY';
|
|
14
|
+
}
|
|
6
15
|
class MariaDbEmailAddressChanger {
|
|
7
16
|
pool;
|
|
8
17
|
now;
|
|
@@ -75,16 +84,25 @@ class MariaDbEmailAddressChanger {
|
|
|
75
84
|
return { status: 'unavailable' };
|
|
76
85
|
const selfAlreadyHasNewHint = hintOwners.length > 0;
|
|
77
86
|
await conn.query('SELECT ic_identity_hint FROM `shield_identity`.`identity_credential` WHERE ic_principal_id = ? FOR UPDATE', [principalId]);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
principalId,
|
|
81
|
-
]);
|
|
87
|
+
// Steps 3–4: a duplicate key here is a row the locked reads could not see (written by a path
|
|
88
|
+
// without the named lock); the index refuses it → `unavailable`, and `change` rolls back.
|
|
82
89
|
let hintMoved = false;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
try {
|
|
91
|
+
await conn.query('UPDATE `shield_app`.`users` SET u_email = ? WHERE u_uuid = ?', [
|
|
92
|
+
newEmail,
|
|
93
|
+
principalId,
|
|
94
|
+
]);
|
|
95
|
+
if (oldEmail !== null && oldEmail !== '' && !selfAlreadyHasNewHint) {
|
|
96
|
+
const moved = (await conn.query(`UPDATE \`shield_identity\`.\`identity_credential\`
|
|
97
|
+
SET ic_identity_hint = ?, ic_updated_at_ms = ?
|
|
98
|
+
WHERE ic_principal_id = ? AND ic_identity_hint = ?`, [newEmail, this.now(), principalId, oldEmail]));
|
|
99
|
+
hintMoved = moved.affectedRows > 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
if (isDuplicateKey(err))
|
|
104
|
+
return { status: 'unavailable' }; // the driver message is dropped
|
|
105
|
+
throw err;
|
|
88
106
|
}
|
|
89
107
|
await conn.query(`UPDATE \`shield_identity\`.\`email_change_token\` SET ect_consumed_at_ms = ?
|
|
90
108
|
WHERE ect_principal_id = ? AND ect_consumed_at_ms IS NULL`, [this.now(), principalId]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MariaDbProfileWriter — the framework-DEFAULT ProfileWriter (backs PATCH /account/profile). Updates
|
|
3
|
-
* ONLY the supplied
|
|
3
|
+
* ONLY the supplied name (`u_username`) on shield_app.users — an `email` is refused (it goes through
|
|
4
|
+
* the verified email-change flow, Phase 12 / KI #87) — then re-reads via MariaDbProfileResolver so the returned
|
|
4
5
|
* display profile has ONE source of truth (names, never UUIDs). Fail-closed: throws if the re-read is null.
|
|
5
6
|
*/
|
|
6
7
|
import type { Pool } from 'mariadb';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mariadb-profile-writer.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-writer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mariadb-profile-writer.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-writer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAOtE,qBAAa,oBAAqB,YAAW,aAAa;IAE5C,OAAO,CAAC,QAAQ,CAAC,IAAI;IADjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;gBACrB,IAAI,EAAE,IAAI;IAIjC,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACzC,OAAO,CAAC,kBAAkB,CAAC;CA0C/B"}
|
|
@@ -10,16 +10,18 @@ class MariaDbProfileWriter {
|
|
|
10
10
|
this.resolver = new mariadb_profile_resolver_js_1.MariaDbProfileResolver(pool);
|
|
11
11
|
}
|
|
12
12
|
async update(principalId, changes) {
|
|
13
|
+
// Phase 12 / KI #87: the email is written ONLY by the verified email-change flow
|
|
14
|
+
// (`MariaDbEmailAddressChanger`: collision refusal, login-hint follow, `uq_users_email`
|
|
15
|
+
// mapping). `PATCH /account/profile` never passes it here; refuse rather than write unchecked.
|
|
16
|
+
if (changes.email !== undefined) {
|
|
17
|
+
throw new Error('MariaDbProfileWriter: email changes go through the verified email-change flow only');
|
|
18
|
+
}
|
|
13
19
|
const sets = [];
|
|
14
20
|
const params = [];
|
|
15
21
|
if (changes.name !== undefined) {
|
|
16
22
|
sets.push('u_username = ?');
|
|
17
23
|
params.push(changes.name);
|
|
18
24
|
}
|
|
19
|
-
if (changes.email !== undefined) {
|
|
20
|
-
sets.push('u_email = ?');
|
|
21
|
-
params.push(changes.email);
|
|
22
|
-
}
|
|
23
25
|
if (sets.length > 0) {
|
|
24
26
|
params.push(principalId);
|
|
25
27
|
await this.pool.query(`UPDATE \`shield_app\`.\`users\` SET ${sets.join(', ')} WHERE u_uuid = ?`, params);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astram/guard",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Astram framework — the enforcement guard + real-world adapters (the 'hands'). One global deny-by-default gate every request passes through (ADR-0002), classification decorators, boot-pool assertion (ADR-0018), the forRoot() registration seam, and the DB/Redis/crypto adapters that connect the sealed core to a real backend. Depends on @astram/core (ADR-0003, ADR-0023).",
|
|
6
6
|
"type": "commonjs",
|