@starci/auth 0.2.1 → 0.4.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/CHANGELOG.md +9 -0
- package/dist/client/auth-client.d.ts +1 -1
- package/dist/client/auth-client.js +7 -4
- package/dist/client/session.d.ts +2 -1
- package/dist/client/session.js +1 -1
- package/dist/contracts/index.d.ts +3 -0
- package/dist/contracts/index.js +1 -0
- package/dist/server/cookie.d.ts +5 -4
- package/dist/server/cookie.js +18 -9
- package/dist/server/doors.js +6 -6
- package/dist/server/options.d.ts +6 -1
- package/dist/server/ports.d.ts +7 -0
- package/dist/server/sessions.js +7 -4
- package/dist/server/tokens.js +3 -1
- package/dist/server/verifier.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-11-04
|
|
4
|
+
|
|
5
|
+
- Added: `RefreshCookieOptions.domains`: the refresh cookie's `Domain` follows the request host (its `Host` header without the port). A host equal to a listed domain, or ending in `.` and one, takes the longest that fits; any other host takes `domain`, else stays host-only, so one api signs in on localhost (host-only) and on a tunnel origin (its parent domain).
|
|
6
|
+
- Changed: `RefreshCookie.issue(response, tokens, request)` and `clear(response, request)` take the request; `AuthDoors` passes it at every write and clear. Specs: `src/server/cookie.spec.ts` (localhost host-only, a tunnel host under a listed domain, the longest fit, an unlisted host and no host fall back).
|
|
7
|
+
|
|
8
|
+
## 0.3.0 - 2026-11-03
|
|
9
|
+
|
|
10
|
+
- Added: a session carries the person's full name when the account directory knows it: `AccountStanding.name` and `AuthSubject.name` in; `SessionUser.name`, the `name` access-token claim (`AUTH_CLAIMS.name`), `AccessClaims.name` and `AuthPrincipal.name` out. The client keeps it (from the grant, or from the token when the grant carries no user) and announces a change of it. An account without a name answers as before.
|
|
11
|
+
|
|
3
12
|
## 0.2.1 - 2026-11-03
|
|
4
13
|
|
|
5
14
|
- Fixed: a refresh replayed inside the grace window no longer signs the person out 30 seconds later. When a rotation's response never reached the browser (a navigation or an abort cut it), the next load presented the spent token, was answered an access token with no cookie (`cookieKept`) and the first load after the grace was reuse. Now `AuthSessions.refresh` gives such a replay a successor of its own in the same session and `AuthDoors.refresh` sets it as the cookie, at most `RACED_SUCCESSORS_MAX` (3) times per rotated token (`raced_count`); one more replay is reuse and ends the session, which revokes every token of it, as sign-out does. `SessionTokens.cookieKept` is removed; the session door never touched the cookie.
|
|
@@ -45,7 +45,7 @@ export interface AuthClient {
|
|
|
45
45
|
accessToken(): Promise<string | undefined>;
|
|
46
46
|
/** The held access token without any request. */
|
|
47
47
|
peek(): string | undefined;
|
|
48
|
-
/** The signed-in person (id and
|
|
48
|
+
/** The signed-in person (id, email and the full name when the account has one), or null when signed out. */
|
|
49
49
|
user(): Promise<SessionUser | null>;
|
|
50
50
|
/** The `Authorization` header of the current session (an empty object when signed out). */
|
|
51
51
|
headers(): Promise<Record<string, string>>;
|
|
@@ -20,11 +20,14 @@ const claimsOf = (token) => {
|
|
|
20
20
|
return {};
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
+
/** The full name of a person, when a grant or a token names one. */
|
|
24
|
+
const nameOf = (name) => (typeof name === "string" ? { name } : {});
|
|
23
25
|
const userOf = (grant) => {
|
|
24
|
-
if (typeof grant.user === "object" && typeof grant.user.id === "string")
|
|
25
|
-
return { id: grant.user.id, email: typeof grant.user.email === "string" ? grant.user.email : "" };
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
if (typeof grant.user === "object" && typeof grant.user.id === "string") {
|
|
27
|
+
return { id: grant.user.id, email: typeof grant.user.email === "string" ? grant.user.email : "", ...nameOf(fieldOf(grant.user, "name")) };
|
|
28
|
+
}
|
|
29
|
+
const { sub, email, name } = claimsOf(grant.accessToken);
|
|
30
|
+
return typeof sub === "string" ? { id: sub, email: typeof email === "string" ? email : "", ...nameOf(name) } : null;
|
|
28
31
|
};
|
|
29
32
|
const MILLISECONDS = 1000;
|
|
30
33
|
const timerPause = (ms) => new Promise((resolve) => {
|
package/dist/client/session.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type SessionEvent = "INITIAL_SESSION" | "SIGNED_IN" | "SIGNED_OUT" | "TOK
|
|
|
4
4
|
export interface SessionPerson {
|
|
5
5
|
readonly id: string;
|
|
6
6
|
readonly email: string;
|
|
7
|
+
readonly name?: string;
|
|
7
8
|
}
|
|
8
9
|
/** What a view reads: loading until the first answer, then signed in or out. The object changes only when the session does. */
|
|
9
10
|
export interface SessionSnapshot<User extends SessionPerson> {
|
|
@@ -33,7 +34,7 @@ export interface SessionStore<User extends SessionPerson> {
|
|
|
33
34
|
getSnapshot: () => SessionSnapshot<User>;
|
|
34
35
|
/** The snapshot for server rendering: always loading. */
|
|
35
36
|
getServerSnapshot: () => SessionSnapshot<User>;
|
|
36
|
-
/** Publishes the snapshot of a signed-in person; watchers hear it only when status, id or
|
|
37
|
+
/** Publishes the snapshot of a signed-in person; watchers hear it only when status, id, email or name changed. */
|
|
37
38
|
signedIn: (user: User | null) => void;
|
|
38
39
|
/** Publishes the signed-out snapshot. */
|
|
39
40
|
signedOut: () => void;
|
package/dist/client/session.js
CHANGED
|
@@ -5,7 +5,7 @@ export const createSessionStore = (source) => {
|
|
|
5
5
|
const listeners = new Set();
|
|
6
6
|
const watchers = new Set();
|
|
7
7
|
const set = (next) => {
|
|
8
|
-
if (snapshot.status === next.status && snapshot.user?.id === next.user?.id && snapshot.user?.email === next.user?.email)
|
|
8
|
+
if (snapshot.status === next.status && snapshot.user?.id === next.user?.id && snapshot.user?.email === next.user?.email && snapshot.user?.name === next.user?.name)
|
|
9
9
|
return;
|
|
10
10
|
snapshot = next;
|
|
11
11
|
for (const watcher of watchers)
|
|
@@ -30,6 +30,7 @@ export declare const AUTH_CLAIMS: {
|
|
|
30
30
|
readonly role: "role";
|
|
31
31
|
readonly sessionId: "session_id";
|
|
32
32
|
readonly email: "email";
|
|
33
|
+
readonly name: "name";
|
|
33
34
|
};
|
|
34
35
|
/** The signature algorithm of every access token: pinned, never negotiated. */
|
|
35
36
|
export declare const AUTH_ALGORITHM = "EdDSA";
|
|
@@ -63,6 +64,8 @@ export interface SessionUser {
|
|
|
63
64
|
readonly id: string;
|
|
64
65
|
/** The account email. */
|
|
65
66
|
readonly email: string;
|
|
67
|
+
/** The person's full name, when the account has one. */
|
|
68
|
+
readonly name?: string;
|
|
66
69
|
}
|
|
67
70
|
/** What a sign-in, refresh or session door answers in the body. */
|
|
68
71
|
export interface AccessGrant {
|
package/dist/contracts/index.js
CHANGED
package/dist/server/cookie.d.ts
CHANGED
|
@@ -2,8 +2,9 @@ import type { HttpRequest, HttpResponse } from "./http.js";
|
|
|
2
2
|
import type { RefreshCookieOptions } from "./options.js";
|
|
3
3
|
import type { DeviceMeta, SessionTokens } from "./ports.js";
|
|
4
4
|
/**
|
|
5
|
-
* The refresh cookie: httpOnly and Secure on every write and clear, SameSite lax (or strict), on the
|
|
6
|
-
* app and the API both see it. The token only ever leaves the back
|
|
5
|
+
* The refresh cookie: httpOnly and Secure on every write and clear, SameSite lax (or strict), on the domain the request host falls
|
|
6
|
+
* under (one of `domains`, else `domain`, else host-only) so the web app and the API both see it. The token only ever leaves the back
|
|
7
|
+
* end through this cookie, never in a body or a URL.
|
|
7
8
|
*/
|
|
8
9
|
export declare class RefreshCookie {
|
|
9
10
|
#private;
|
|
@@ -13,7 +14,7 @@ export declare class RefreshCookie {
|
|
|
13
14
|
/** The device a request comes from: its user agent (bounded) and caller address. */
|
|
14
15
|
deviceOf(request: HttpRequest): DeviceMeta;
|
|
15
16
|
/** Sets the cookie to the refresh token of a session, clearing the copies older sessions kept under their paths. */
|
|
16
|
-
issue(response: HttpResponse, tokens: SessionTokens): void;
|
|
17
|
+
issue(response: HttpResponse, tokens: SessionTokens, request: HttpRequest): void;
|
|
17
18
|
/** Clears the cookie on its path and on the paths older sessions used. */
|
|
18
|
-
clear(response: HttpResponse): void;
|
|
19
|
+
clear(response: HttpResponse, request: HttpRequest): void;
|
|
19
20
|
}
|
package/dist/server/cookie.js
CHANGED
|
@@ -2,8 +2,9 @@ import { cookieValue } from "./primitives.js";
|
|
|
2
2
|
const MAX_AGENT_LENGTH = 512;
|
|
3
3
|
const MILLISECONDS = 1000;
|
|
4
4
|
/**
|
|
5
|
-
* The refresh cookie: httpOnly and Secure on every write and clear, SameSite lax (or strict), on the
|
|
6
|
-
* app and the API both see it. The token only ever leaves the back
|
|
5
|
+
* The refresh cookie: httpOnly and Secure on every write and clear, SameSite lax (or strict), on the domain the request host falls
|
|
6
|
+
* under (one of `domains`, else `domain`, else host-only) so the web app and the API both see it. The token only ever leaves the back
|
|
7
|
+
* end through this cookie, never in a body or a URL.
|
|
7
8
|
*/
|
|
8
9
|
export class RefreshCookie {
|
|
9
10
|
#options;
|
|
@@ -22,18 +23,27 @@ export class RefreshCookie {
|
|
|
22
23
|
return { userAgent: text?.slice(0, MAX_AGENT_LENGTH) ?? null, ip: request.ip ?? null };
|
|
23
24
|
}
|
|
24
25
|
/** Sets the cookie to the refresh token of a session, clearing the copies older sessions kept under their paths. */
|
|
25
|
-
issue(response, tokens) {
|
|
26
|
+
issue(response, tokens, request) {
|
|
27
|
+
const domain = this.#domainOf(request);
|
|
26
28
|
for (const path of this.#legacyPaths())
|
|
27
|
-
response.clearCookie(this.#options.name, this.#flags(path));
|
|
29
|
+
response.clearCookie(this.#options.name, this.#flags(path, domain));
|
|
28
30
|
response.cookie(this.#options.name, tokens.cookieToken, {
|
|
29
|
-
...this.#flags(this.#path()),
|
|
31
|
+
...this.#flags(this.#path(), domain),
|
|
30
32
|
maxAge: tokens.refreshMaxAgeSeconds * MILLISECONDS,
|
|
31
33
|
});
|
|
32
34
|
}
|
|
33
35
|
/** Clears the cookie on its path and on the paths older sessions used. */
|
|
34
|
-
clear(response) {
|
|
36
|
+
clear(response, request) {
|
|
37
|
+
const domain = this.#domainOf(request);
|
|
35
38
|
for (const path of [this.#path(), ...this.#legacyPaths()])
|
|
36
|
-
response.clearCookie(this.#options.name, this.#flags(path));
|
|
39
|
+
response.clearCookie(this.#options.name, this.#flags(path, domain));
|
|
40
|
+
}
|
|
41
|
+
/** The `Domain` for a request: the longest listed domain its host (without the port) equals or ends under, else the configured one. */
|
|
42
|
+
#domainOf(request) {
|
|
43
|
+
const header = request.headers.host;
|
|
44
|
+
const host = (Array.isArray(header) ? header[0] : header)?.replace(/:\d+$/u, "").toLowerCase() ?? "";
|
|
45
|
+
const fits = (this.#options.domains ?? []).filter((domain) => host === domain || host.endsWith(`.${domain}`));
|
|
46
|
+
return fits.sort((a, b) => b.length - a.length)[0] ?? this.#options.domain;
|
|
37
47
|
}
|
|
38
48
|
#path() {
|
|
39
49
|
return this.#options.path ?? "/";
|
|
@@ -41,8 +51,7 @@ export class RefreshCookie {
|
|
|
41
51
|
#legacyPaths() {
|
|
42
52
|
return (this.#options.legacyPaths ?? ["/auth"]).filter((path) => path !== this.#path());
|
|
43
53
|
}
|
|
44
|
-
#flags(path) {
|
|
45
|
-
const domain = this.#options.domain;
|
|
54
|
+
#flags(path, domain) {
|
|
46
55
|
return {
|
|
47
56
|
httpOnly: true,
|
|
48
57
|
secure: true,
|
package/dist/server/doors.js
CHANGED
|
@@ -22,18 +22,18 @@ export class AuthDoors {
|
|
|
22
22
|
async login(exchange) {
|
|
23
23
|
const subject = await this.#deps.flows.authenticateLocal(exchange);
|
|
24
24
|
const tokens = await this.#deps.sessions.start(subject, this.#deps.cookie.deviceOf(exchange.req));
|
|
25
|
-
this.#deps.cookie.issue(exchange.res, tokens);
|
|
25
|
+
this.#deps.cookie.issue(exchange.res, tokens, exchange.req);
|
|
26
26
|
return accessOf(tokens);
|
|
27
27
|
}
|
|
28
28
|
/** Rotates the cookie token and sets the successor (every refresh rotates, a racing one included); a cookie that cannot be used is cleared and the refusal rethrown. */
|
|
29
29
|
async refresh(exchange) {
|
|
30
30
|
try {
|
|
31
31
|
const tokens = await this.#deps.sessions.refresh(this.#deps.cookie.read(exchange.req));
|
|
32
|
-
this.#deps.cookie.issue(exchange.res, tokens);
|
|
32
|
+
this.#deps.cookie.issue(exchange.res, tokens, exchange.req);
|
|
33
33
|
return accessOf(tokens);
|
|
34
34
|
}
|
|
35
35
|
catch (cause) {
|
|
36
|
-
this.#deps.cookie.clear(exchange.res);
|
|
36
|
+
this.#deps.cookie.clear(exchange.res, exchange.req);
|
|
37
37
|
throw cause;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -44,7 +44,7 @@ export class AuthDoors {
|
|
|
44
44
|
/** Ends the session of the cookie and clears it. */
|
|
45
45
|
async logout(exchange) {
|
|
46
46
|
await this.#deps.sessions.end(this.#deps.cookie.read(exchange.req));
|
|
47
|
-
this.#deps.cookie.clear(exchange.res);
|
|
47
|
+
this.#deps.cookie.clear(exchange.res, exchange.req);
|
|
48
48
|
}
|
|
49
49
|
/** Sends the browser to the Google consent screen. */
|
|
50
50
|
beginGoogle(exchange) {
|
|
@@ -59,11 +59,11 @@ export class AuthDoors {
|
|
|
59
59
|
try {
|
|
60
60
|
const completion = await this.#deps.flows.completeGoogle(exchange);
|
|
61
61
|
const tokens = await this.#deps.sessions.start(completion.subject, this.#deps.cookie.deviceOf(exchange.req));
|
|
62
|
-
this.#deps.cookie.issue(exchange.res, tokens);
|
|
62
|
+
this.#deps.cookie.issue(exchange.res, tokens, exchange.req);
|
|
63
63
|
return { url: `${this.#deps.returnOrigin}${completion.next}`, statusCode: SEE_OTHER };
|
|
64
64
|
}
|
|
65
65
|
catch (cause) {
|
|
66
|
-
this.#deps.cookie.clear(exchange.res);
|
|
66
|
+
this.#deps.cookie.clear(exchange.res, exchange.req);
|
|
67
67
|
this.#deps.logger?.warn(GOOGLE_SIGN_IN_FAILED_EVENT, { cause: String(cause) });
|
|
68
68
|
return { url: `${this.#deps.returnOrigin}/sign-in?auth=error`, statusCode: SEE_OTHER };
|
|
69
69
|
}
|
package/dist/server/options.d.ts
CHANGED
|
@@ -24,8 +24,13 @@ export interface GoogleOptions {
|
|
|
24
24
|
export interface RefreshCookieOptions {
|
|
25
25
|
/** The cookie name. */
|
|
26
26
|
readonly name: string;
|
|
27
|
-
/** The `Domain` of the cookie
|
|
27
|
+
/** The `Domain` of the cookie when the request host is under none of `domains`; undefined keeps it host-only. */
|
|
28
28
|
readonly domain?: string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The domains a request host may set the cookie on: a host equal to one, or ending in "." and one, gets that one as the `Domain`
|
|
31
|
+
* (the longest that fits), so one api serves sign-in on localhost (host-only) and on a tunnel origin (its parent domain).
|
|
32
|
+
*/
|
|
33
|
+
readonly domains?: readonly string[] | undefined;
|
|
29
34
|
/** The path the cookie is sent on (the web app's server reads it on its own pages and forwards it to the api). */
|
|
30
35
|
readonly path?: string | undefined;
|
|
31
36
|
/** The paths older sessions wrote the cookie under; each is cleared so a stale copy never shadows the live one. */
|
package/dist/server/ports.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface AuthSubject {
|
|
|
5
5
|
readonly userId: string;
|
|
6
6
|
/** The account email. */
|
|
7
7
|
readonly email: string;
|
|
8
|
+
/** The person's full name, when the account has one. */
|
|
9
|
+
readonly name?: string | undefined;
|
|
8
10
|
}
|
|
9
11
|
/** What Google told us about the person who consented. */
|
|
10
12
|
export interface GoogleProfile {
|
|
@@ -34,6 +36,8 @@ export interface AccountStanding {
|
|
|
34
36
|
readonly email: string | null;
|
|
35
37
|
/** Whether the account is disabled or gone. */
|
|
36
38
|
readonly disabled: boolean;
|
|
39
|
+
/** The person's full name, when the account has one (the session answers and the access token carry it). */
|
|
40
|
+
readonly name?: string | undefined;
|
|
37
41
|
}
|
|
38
42
|
/** The app's user table, seen only as far as sessions need it. */
|
|
39
43
|
export interface AccountDirectory {
|
|
@@ -130,6 +134,8 @@ export interface AccessClaims {
|
|
|
130
134
|
readonly email?: string | undefined;
|
|
131
135
|
/** The device session the token belongs to. */
|
|
132
136
|
readonly sessionId: string;
|
|
137
|
+
/** The person's full name at issue time, when the account has one. */
|
|
138
|
+
readonly name?: string | undefined;
|
|
133
139
|
}
|
|
134
140
|
/** The principal domain code builds from a verified access token. */
|
|
135
141
|
export interface AuthPrincipal {
|
|
@@ -137,6 +143,7 @@ export interface AuthPrincipal {
|
|
|
137
143
|
readonly role: "authenticated";
|
|
138
144
|
readonly sessionId: string;
|
|
139
145
|
readonly email?: string;
|
|
146
|
+
readonly name?: string;
|
|
140
147
|
}
|
|
141
148
|
/** A freshly signed access token. */
|
|
142
149
|
export interface IssuedAccessToken {
|
package/dist/server/sessions.js
CHANGED
|
@@ -12,6 +12,8 @@ export const RACED_SUCCESSORS_MAX = 3;
|
|
|
12
12
|
/** The log event of a replayed refresh token. */
|
|
13
13
|
export const REFRESH_REUSE_EVENT = "auth.refresh-reuse-detected";
|
|
14
14
|
const MILLISECONDS = 1000;
|
|
15
|
+
/** The subject of a standing account: its id, its address and its full name when the directory knows one. */
|
|
16
|
+
const subjectOf = (user, email) => ({ userId: user.id, email, ...(user.name === undefined ? {} : { name: user.name }) });
|
|
15
17
|
/**
|
|
16
18
|
* Device sessions: opens one at sign-in, rotates its refresh token on every refresh (a replayed token ends the session) and ends it
|
|
17
19
|
* at sign-out. A token rotated within the grace window gets a successor of its own in the same session, at most
|
|
@@ -92,7 +94,7 @@ export class AuthSessions {
|
|
|
92
94
|
throw new AuthError(AUTH_ERROR_CODES.AccountDisabled);
|
|
93
95
|
}
|
|
94
96
|
await this.#store.touchSession(rotation.row.sessionId, now);
|
|
95
|
-
return this.#tokensFor(
|
|
97
|
+
return this.#tokensFor(subjectOf(user, user.email), rotation.row.sessionId, rotation.next, now);
|
|
96
98
|
}
|
|
97
99
|
/**
|
|
98
100
|
* Answers a fresh access token for the session of a refresh token WITHOUT rotating it, so parallel gated requests of one browser
|
|
@@ -138,17 +140,18 @@ export class AuthSessions {
|
|
|
138
140
|
await this.#store.closeSession(row.sessionId);
|
|
139
141
|
throw new AuthError(AUTH_ERROR_CODES.AccountDisabled);
|
|
140
142
|
}
|
|
141
|
-
return this.#tokensFor(
|
|
143
|
+
return this.#tokensFor(subjectOf(user, user.email), row.sessionId, presented, now);
|
|
142
144
|
}
|
|
143
145
|
async #tokensFor(subject, sessionId, refreshToken, now) {
|
|
144
|
-
const
|
|
146
|
+
const name = subject.name === undefined ? {} : { name: subject.name };
|
|
147
|
+
const access = await this.#deps.tokens.sign({ sub: subject.userId, email: subject.email, sessionId, ...name }, now);
|
|
145
148
|
return {
|
|
146
149
|
accessToken: access.token,
|
|
147
150
|
accessExpiresAt: access.expiresAt.toISOString(),
|
|
148
151
|
accessExpiresIn: Math.round((access.expiresAt.getTime() - now.getTime()) / MILLISECONDS),
|
|
149
152
|
cookieToken: refreshToken,
|
|
150
153
|
refreshMaxAgeSeconds: this.#deps.options.refreshTtlSeconds,
|
|
151
|
-
user: { id: subject.userId, email: subject.email },
|
|
154
|
+
user: { id: subject.userId, email: subject.email, ...name },
|
|
152
155
|
};
|
|
153
156
|
}
|
|
154
157
|
#refreshExpiry(now) {
|
package/dist/server/tokens.js
CHANGED
|
@@ -25,6 +25,7 @@ export class AuthTokens {
|
|
|
25
25
|
[AUTH_CLAIMS.role]: AUTH_ROLE,
|
|
26
26
|
[AUTH_CLAIMS.sessionId]: claims.sessionId,
|
|
27
27
|
...(claims.email === undefined ? {} : { [AUTH_CLAIMS.email]: claims.email }),
|
|
28
|
+
...(claims.name === undefined ? {} : { [AUTH_CLAIMS.name]: claims.name }),
|
|
28
29
|
})
|
|
29
30
|
.setProtectedHeader({ alg: AUTH_ALGORITHM, kid: signer.keyId, typ: "JWT" })
|
|
30
31
|
.setSubject(claims.sub)
|
|
@@ -50,7 +51,8 @@ export class AuthTokens {
|
|
|
50
51
|
return refused(AUTH_ERROR_CODES.AccessTokenInvalid);
|
|
51
52
|
}
|
|
52
53
|
const email = payload[AUTH_CLAIMS.email];
|
|
53
|
-
|
|
54
|
+
const name = payload[AUTH_CLAIMS.name];
|
|
55
|
+
return ok({ sub: payload.sub, sessionId, ...(typeof email === "string" ? { email } : {}), ...(typeof name === "string" ? { name } : {}) });
|
|
54
56
|
}
|
|
55
57
|
catch (cause) {
|
|
56
58
|
return refused(AUTH_ERROR_CODES.AccessTokenInvalid, { cause: String(cause) });
|
package/dist/server/verifier.js
CHANGED
|
@@ -13,8 +13,8 @@ export class AuthPrincipalVerifier {
|
|
|
13
13
|
const verified = await this.#tokens.verify(token, now);
|
|
14
14
|
if (verified.kind === "refused")
|
|
15
15
|
return refused(AUTH_ERROR_CODES.AccessTokenInvalid);
|
|
16
|
-
const { sub, email, sessionId } = verified.value;
|
|
17
|
-
return ok({ id: sub, role: AUTH_ROLE, sessionId, ...(email === undefined ? {} : { email }) });
|
|
16
|
+
const { sub, email, name, sessionId } = verified.value;
|
|
17
|
+
return ok({ id: sub, role: AUTH_ROLE, sessionId, ...(email === undefined ? {} : { email }), ...(name === undefined ? {} : { name }) });
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
/** The verifier of an app that serves no user session: every access token is refused, so the default-deny guard admits only its public and credential doors. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The sign-in of a StarCi lite app in one package: a NestJS module (EdDSA access tokens with a multi-key JWKS, bcrypt passwords, Google OAuth with state and PKCE, rotating refresh tokens with a reuse grace window and reuse detection, a failures-only sign-in limit, the refresh cookie), the door contracts, a front-end auth client (access token in memory, one shared refresh, signed out only on 401 or 403) with React bindings, and test doubles.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|