@chipmobilesdk/rn-auth 0.3.0 → 0.3.1
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 +126 -113
- package/README.md +190 -190
- package/package.json +51 -54
- package/skills/sdk-rn-auth/SKILL.md +62 -63
- package/src/access/accessTier.ts +88 -88
- package/src/config/types.ts +154 -154
- package/src/config/validateConfig.ts +114 -108
- package/src/core/authMachine.ts +364 -364
- package/src/core/profile.ts +57 -60
- package/src/core/session.ts +105 -112
- package/src/diagnostics/errors.ts +87 -85
- package/src/diagnostics/messageKeys.ts +59 -61
- package/src/index.ts +71 -83
- package/src/oidc/authClient.ts +195 -199
- package/src/oidc/classifyError.ts +76 -76
- package/src/oidc/socialRouting.ts +66 -73
- package/src/react/AuthProvider.tsx +41 -47
- package/src/react/hooks.ts +86 -91
- package/src/tokens/accessToken.ts +159 -164
- package/src/tokens/secureStore.ts +51 -52
package/src/core/profile.ts
CHANGED
|
@@ -1,60 +1,57 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claims → Profile mapping (FR-006). Tolerant of Apple private-relay emails and
|
|
3
|
-
* missing display name / given name (edge cases).
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Profile, ProviderId, ProviderIdentity } from '../config/types';
|
|
7
|
-
|
|
8
|
-
function asString(value: unknown): string | undefined {
|
|
9
|
-
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function looksLikeEmail(value: string | undefined): boolean {
|
|
13
|
-
return !!value && /.+@.+\..+/.test(value);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Map id-token claims to the app-visible Profile. */
|
|
17
|
-
export function mapClaimsToProfile(
|
|
18
|
-
claims: Record<string, unknown>,
|
|
19
|
-
providerId?: ProviderId,
|
|
20
|
-
): Profile {
|
|
21
|
-
const tenantUserId =
|
|
22
|
-
asString(claims.oid) ?? asString(claims.sub) ?? asString(claims.unique_name) ?? 'unknown';
|
|
23
|
-
|
|
24
|
-
const givenName = asString(claims.given_name);
|
|
25
|
-
const familyName = asString(claims.family_name);
|
|
26
|
-
const composedName = [givenName, familyName].filter(Boolean).join(' ') || undefined;
|
|
27
|
-
const displayName = asString(claims.name) ?? composedName;
|
|
28
|
-
|
|
29
|
-
const preferred = asString(claims.preferred_username);
|
|
30
|
-
const email =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
)
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
return { provider, subject, emailBehavior };
|
|
60
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Claims → Profile mapping (FR-006). Tolerant of Apple private-relay emails and
|
|
3
|
+
* missing display name / given name (edge cases).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Profile, ProviderId, ProviderIdentity } from '../config/types';
|
|
7
|
+
|
|
8
|
+
function asString(value: unknown): string | undefined {
|
|
9
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function looksLikeEmail(value: string | undefined): boolean {
|
|
13
|
+
return !!value && /.+@.+\..+/.test(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Map id-token claims to the app-visible Profile. */
|
|
17
|
+
export function mapClaimsToProfile(
|
|
18
|
+
claims: Record<string, unknown>,
|
|
19
|
+
providerId?: ProviderId,
|
|
20
|
+
): Profile {
|
|
21
|
+
const tenantUserId =
|
|
22
|
+
asString(claims.oid) ?? asString(claims.sub) ?? asString(claims.unique_name) ?? 'unknown';
|
|
23
|
+
|
|
24
|
+
const givenName = asString(claims.given_name);
|
|
25
|
+
const familyName = asString(claims.family_name);
|
|
26
|
+
const composedName = [givenName, familyName].filter(Boolean).join(' ') || undefined;
|
|
27
|
+
const displayName = asString(claims.name) ?? composedName;
|
|
28
|
+
|
|
29
|
+
const preferred = asString(claims.preferred_username);
|
|
30
|
+
const email = asString(claims.email) ?? (looksLikeEmail(preferred) ? preferred : undefined);
|
|
31
|
+
|
|
32
|
+
const provider =
|
|
33
|
+
providerId ?? asString(claims.idp) ?? asString(claims.identityProvider) ?? 'unknown';
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
tenantUserId,
|
|
37
|
+
displayName,
|
|
38
|
+
email,
|
|
39
|
+
provider,
|
|
40
|
+
claims,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Describe the external provider identity behind the profile (FR-004 edge cases). */
|
|
45
|
+
export function describeProviderIdentity(
|
|
46
|
+
claims: Record<string, unknown>,
|
|
47
|
+
providerId?: ProviderId,
|
|
48
|
+
): ProviderIdentity {
|
|
49
|
+
const provider = providerId ?? asString(claims.idp) ?? 'unknown';
|
|
50
|
+
const subject = asString(claims.sub) ?? asString(claims.oid) ?? 'unknown';
|
|
51
|
+
const email = asString(claims.email);
|
|
52
|
+
let emailBehavior: ProviderIdentity['emailBehavior'] = 'none';
|
|
53
|
+
if (email) {
|
|
54
|
+
emailBehavior = email.endsWith('@privaterelay.appleid.com') ? 'private-relay' : 'direct';
|
|
55
|
+
}
|
|
56
|
+
return { provider, subject, emailBehavior };
|
|
57
|
+
}
|
package/src/core/session.ts
CHANGED
|
@@ -1,112 +1,105 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session restoration (FR-008, R5). Reads the persisted artifact, attempts a
|
|
3
|
-
* silent refresh, and classifies the outcome:
|
|
4
|
-
* - restored: fresh tokens + claims → authenticated
|
|
5
|
-
* - offline: the refresh could not complete for any reason other than the
|
|
6
|
-
* session being invalid — no network, a timeout, a 5xx, an error we
|
|
7
|
-
* cannot classify → cached profile read-only, tier unverified (fail
|
|
8
|
-
* closed) — clarify Q3
|
|
9
|
-
* - unavailable: no artifact, or the provider said the refresh token is no
|
|
10
|
-
* longer valid → signed-out reauth
|
|
11
|
-
*
|
|
12
|
-
* Only `unavailable` deletes the stored session, so it is reserved for a verdict
|
|
13
|
-
* the provider actually gave. Treating every unclassified error as expiry signed
|
|
14
|
-
* users out whenever the token endpoint had a bad moment.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from '../
|
|
23
|
-
import
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
profile: persisted.profile,
|
|
107
|
-
accessTier: failClosedTier(),
|
|
108
|
-
refreshToken: persisted.refreshToken,
|
|
109
|
-
idToken: persisted.idToken,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Session restoration (FR-008, R5). Reads the persisted artifact, attempts a
|
|
3
|
+
* silent refresh, and classifies the outcome:
|
|
4
|
+
* - restored: fresh tokens + claims → authenticated
|
|
5
|
+
* - offline: the refresh could not complete for any reason other than the
|
|
6
|
+
* session being invalid — no network, a timeout, a 5xx, an error we
|
|
7
|
+
* cannot classify → cached profile read-only, tier unverified (fail
|
|
8
|
+
* closed) — clarify Q3
|
|
9
|
+
* - unavailable: no artifact, or the provider said the refresh token is no
|
|
10
|
+
* longer valid → signed-out reauth
|
|
11
|
+
*
|
|
12
|
+
* Only `unavailable` deletes the stored session, so it is reserved for a verdict
|
|
13
|
+
* the provider actually gave. Treating every unclassified error as expiry signed
|
|
14
|
+
* users out whenever the token endpoint had a bad moment.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { AccessTier, AccessTierMapping, Profile, RestoreStatus } from '../config/types';
|
|
18
|
+
import type { OidcAuthClient } from '../oidc/authClient';
|
|
19
|
+
import type { SecureTokenStore } from '../tokens/secureStore';
|
|
20
|
+
import { AUTH_STORAGE_KEY } from '../tokens/secureStore';
|
|
21
|
+
import { mapClaimsToProfile } from './profile';
|
|
22
|
+
import { evaluateAccessTier, failClosedTier } from '../access/accessTier';
|
|
23
|
+
import { isAuthError } from '../diagnostics/errors';
|
|
24
|
+
|
|
25
|
+
export interface PersistedSession {
|
|
26
|
+
refreshToken: string;
|
|
27
|
+
profile: Profile;
|
|
28
|
+
/** The id token hint for end-session (AUTH-04). Absent in artifacts written before 0.2.2. */
|
|
29
|
+
idToken?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RestoreDeps {
|
|
33
|
+
secureStore: SecureTokenStore;
|
|
34
|
+
authority: string;
|
|
35
|
+
clientId: string;
|
|
36
|
+
redirectUrl: string;
|
|
37
|
+
scopes: string[];
|
|
38
|
+
authClient: OidcAuthClient;
|
|
39
|
+
mapping: AccessTierMapping;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RestoreResult {
|
|
43
|
+
outcome: RestoreStatus;
|
|
44
|
+
profile?: Profile;
|
|
45
|
+
accessTier?: AccessTier;
|
|
46
|
+
refreshToken?: string;
|
|
47
|
+
idToken?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function parsePersistedSession(raw: string | null): PersistedSession | null {
|
|
51
|
+
if (!raw) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(raw) as PersistedSession;
|
|
56
|
+
if (parsed && typeof parsed.refreshToken === 'string' && parsed.profile) {
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function restoreSession(deps: RestoreDeps): Promise<RestoreResult> {
|
|
66
|
+
const raw = await deps.secureStore.getItem(AUTH_STORAGE_KEY);
|
|
67
|
+
const persisted = parsePersistedSession(raw);
|
|
68
|
+
if (!persisted) {
|
|
69
|
+
return { outcome: 'unavailable' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const tokens = await deps.authClient.refresh({
|
|
74
|
+
authority: deps.authority,
|
|
75
|
+
clientId: deps.clientId,
|
|
76
|
+
redirectUrl: deps.redirectUrl,
|
|
77
|
+
refreshToken: persisted.refreshToken,
|
|
78
|
+
scopes: deps.scopes,
|
|
79
|
+
});
|
|
80
|
+
// A refresh response may carry no id token, and so no claims. The stored
|
|
81
|
+
// profile is then still the best identity there is; an empty one is not.
|
|
82
|
+
const hasClaims = Object.keys(tokens.claims).length > 0;
|
|
83
|
+
return {
|
|
84
|
+
outcome: 'restored',
|
|
85
|
+
profile: hasClaims ? mapClaimsToProfile(tokens.claims) : persisted.profile,
|
|
86
|
+
accessTier: hasClaims ? evaluateAccessTier(tokens.claims, deps.mapping) : failClosedTier(),
|
|
87
|
+
refreshToken: tokens.refreshToken || persisted.refreshToken,
|
|
88
|
+
idToken: tokens.idToken || persisted.idToken,
|
|
89
|
+
};
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (isAuthError(error) && error.code === 'session-expired') {
|
|
92
|
+
// The provider rejected the refresh token: reauthentication is required.
|
|
93
|
+
return { outcome: 'unavailable' };
|
|
94
|
+
}
|
|
95
|
+
// Anything else is no verdict on the session. Keep the cached identity
|
|
96
|
+
// read-only and fail closed on entitlement; the next restore tries again.
|
|
97
|
+
return {
|
|
98
|
+
outcome: 'offline',
|
|
99
|
+
profile: persisted.profile,
|
|
100
|
+
accessTier: failClosedTier(),
|
|
101
|
+
refreshToken: persisted.refreshToken,
|
|
102
|
+
idToken: persisted.idToken,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -1,85 +1,87 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Developer-facing diagnostics. AuthError carries a stable machine-readable
|
|
3
|
-
* code, an audience (user vs developer/tenant), and a localizable message key.
|
|
4
|
-
* `detail` is always passed through `redact` so tokens and secrets never appear
|
|
5
|
-
* in logs or error objects (FR-014).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export type AuthErrorCode =
|
|
9
|
-
| 'configuration-error'
|
|
10
|
-
| 'provider-cancelled'
|
|
11
|
-
| 'redirect-mismatch'
|
|
12
|
-
| 'token-acquisition-failed'
|
|
13
|
-
| 'session-expired'
|
|
14
|
-
| 'consent-denied'
|
|
15
|
-
| 'entitlement-unverified'
|
|
16
|
-
| 'network-unavailable';
|
|
17
|
-
|
|
18
|
-
export type AuthErrorAudience = 'user' | 'developer';
|
|
19
|
-
|
|
20
|
-
export interface AuthErrorArgs {
|
|
21
|
-
code: AuthErrorCode;
|
|
22
|
-
messageKey: string;
|
|
23
|
-
audience?: AuthErrorAudience;
|
|
24
|
-
detail?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Replace token-like substrings (long URL-safe base64 / JWT segments) with a
|
|
29
|
-
* placeholder so diagnostics never leak secrets or token values (FR-014).
|
|
30
|
-
*/
|
|
31
|
-
export function redact(value: string | undefined): string | undefined {
|
|
32
|
-
if (!value) {
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
readonly
|
|
46
|
-
readonly
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Developer-facing diagnostics. AuthError carries a stable machine-readable
|
|
3
|
+
* code, an audience (user vs developer/tenant), and a localizable message key.
|
|
4
|
+
* `detail` is always passed through `redact` so tokens and secrets never appear
|
|
5
|
+
* in logs or error objects (FR-014).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type AuthErrorCode =
|
|
9
|
+
| 'configuration-error'
|
|
10
|
+
| 'provider-cancelled'
|
|
11
|
+
| 'redirect-mismatch'
|
|
12
|
+
| 'token-acquisition-failed'
|
|
13
|
+
| 'session-expired'
|
|
14
|
+
| 'consent-denied'
|
|
15
|
+
| 'entitlement-unverified'
|
|
16
|
+
| 'network-unavailable';
|
|
17
|
+
|
|
18
|
+
export type AuthErrorAudience = 'user' | 'developer';
|
|
19
|
+
|
|
20
|
+
export interface AuthErrorArgs {
|
|
21
|
+
code: AuthErrorCode;
|
|
22
|
+
messageKey: string;
|
|
23
|
+
audience?: AuthErrorAudience;
|
|
24
|
+
detail?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Replace token-like substrings (long URL-safe base64 / JWT segments) with a
|
|
29
|
+
* placeholder so diagnostics never leak secrets or token values (FR-014).
|
|
30
|
+
*/
|
|
31
|
+
export function redact(value: string | undefined): string | undefined {
|
|
32
|
+
if (!value) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return (
|
|
36
|
+
value
|
|
37
|
+
// JWT-shaped values: three base64url segments separated by dots.
|
|
38
|
+
.replace(/[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, '[redacted-token]')
|
|
39
|
+
// Any other long opaque token-like run.
|
|
40
|
+
.replace(/[A-Za-z0-9_-]{24,}/g, '[redacted]')
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class AuthError extends Error {
|
|
45
|
+
readonly code: AuthErrorCode;
|
|
46
|
+
readonly audience: AuthErrorAudience;
|
|
47
|
+
readonly messageKey: string;
|
|
48
|
+
readonly detail?: string;
|
|
49
|
+
|
|
50
|
+
constructor(args: AuthErrorArgs) {
|
|
51
|
+
super(args.messageKey);
|
|
52
|
+
this.name = 'AuthError';
|
|
53
|
+
this.code = args.code;
|
|
54
|
+
this.audience = args.audience ?? 'developer';
|
|
55
|
+
this.messageKey = args.messageKey;
|
|
56
|
+
this.detail = redact(args.detail);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class AuthConfigError extends AuthError {
|
|
61
|
+
constructor(messageKey: string, detail?: string) {
|
|
62
|
+
super({ code: 'configuration-error', messageKey, audience: 'developer', detail });
|
|
63
|
+
this.name = 'AuthConfigError';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function isAuthError(value: unknown): value is AuthError {
|
|
68
|
+
return value instanceof AuthError;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Normalize any thrown value into an AuthError without leaking secrets. */
|
|
72
|
+
export function toAuthError(
|
|
73
|
+
value: unknown,
|
|
74
|
+
fallbackCode: AuthErrorCode = 'token-acquisition-failed',
|
|
75
|
+
fallbackMessageKey = 'auth.error.token',
|
|
76
|
+
): AuthError {
|
|
77
|
+
if (isAuthError(value)) {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
const message = value instanceof Error ? value.message : String(value);
|
|
81
|
+
return new AuthError({
|
|
82
|
+
code: fallbackCode,
|
|
83
|
+
messageKey: fallbackMessageKey,
|
|
84
|
+
audience: 'developer',
|
|
85
|
+
detail: message,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -1,61 +1,59 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stable, localizable message keys with English default copy (UX-006 / LIB-011).
|
|
3
|
-
* The library takes no i18n runtime dependency: consuming apps map these keys to
|
|
4
|
-
* localized strings via their own i18n system. User-actionable copy and
|
|
5
|
-
* developer/tenant-configuration copy are kept separate (UX-005).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface AuthMessageMap {
|
|
9
|
-
// User-facing actions and states
|
|
10
|
-
'auth.action.continueGoogle': string;
|
|
11
|
-
'auth.action.continueApple': string;
|
|
12
|
-
'auth.action.continueFacebook': string;
|
|
13
|
-
'auth.state.signedOut': string;
|
|
14
|
-
'auth.state.authenticating': string;
|
|
15
|
-
'auth.state.signedIn': string;
|
|
16
|
-
'auth.state.signingOut': string;
|
|
17
|
-
// Access tier (user-facing)
|
|
18
|
-
'auth.tier.free': string;
|
|
19
|
-
'auth.tier.premium': string;
|
|
20
|
-
'auth.tier.unverified': string;
|
|
21
|
-
// User-actionable errors
|
|
22
|
-
'auth.error.cancelled': string;
|
|
23
|
-
'auth.error.sessionExpired': string;
|
|
24
|
-
'auth.error.entitlement': string;
|
|
25
|
-
'auth.error.network': string;
|
|
26
|
-
// Developer / tenant-configuration errors
|
|
27
|
-
'auth.error.configuration': string;
|
|
28
|
-
'auth.error.redirectMismatch': string;
|
|
29
|
-
'auth.error.token': string;
|
|
30
|
-
'auth.error.consentDenied': string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type AuthMessageKey = keyof AuthMessageMap;
|
|
34
|
-
|
|
35
|
-
export const DEFAULT_MESSAGES: AuthMessageMap = {
|
|
36
|
-
'auth.action.continueGoogle': 'Continue with Google',
|
|
37
|
-
'auth.action.continueApple': 'Continue with Apple',
|
|
38
|
-
'auth.action.continueFacebook': 'Continue with Facebook',
|
|
39
|
-
'auth.state.signedOut': 'Sign in to continue',
|
|
40
|
-
'auth.state.authenticating': 'Signing you in…',
|
|
41
|
-
'auth.state.signedIn': 'You are signed in',
|
|
42
|
-
'auth.state.signingOut': 'Signing out…',
|
|
43
|
-
'auth.tier.free': 'Free',
|
|
44
|
-
'auth.tier.premium': 'Premium',
|
|
45
|
-
'auth.tier.unverified': 'Membership could not be verified',
|
|
46
|
-
'auth.error.cancelled': 'Sign-in was cancelled. You can try again.',
|
|
47
|
-
'auth.error.sessionExpired': 'Your session expired. Please sign in again.',
|
|
48
|
-
'auth.error.entitlement': 'This feature requires a verified premium membership.',
|
|
49
|
-
'auth.error.network': 'No connection. Some features are limited until you reconnect.',
|
|
50
|
-
'auth.error.configuration': 'Authentication is not configured correctly.',
|
|
51
|
-
'auth.error.redirectMismatch': 'The sign-in redirect does not match the app configuration.',
|
|
52
|
-
'auth.error.token': 'We could not complete the request. Please try again.',
|
|
53
|
-
'auth.error.consentDenied': 'Access to this resource was denied.',
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/** Resolve a message, preferring app overrides over the English defaults. */
|
|
57
|
-
export function resolveMessages(
|
|
58
|
-
overrides
|
|
59
|
-
|
|
60
|
-
return { ...DEFAULT_MESSAGES, ...(overrides ?? {}) };
|
|
61
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Stable, localizable message keys with English default copy (UX-006 / LIB-011).
|
|
3
|
+
* The library takes no i18n runtime dependency: consuming apps map these keys to
|
|
4
|
+
* localized strings via their own i18n system. User-actionable copy and
|
|
5
|
+
* developer/tenant-configuration copy are kept separate (UX-005).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface AuthMessageMap {
|
|
9
|
+
// User-facing actions and states
|
|
10
|
+
'auth.action.continueGoogle': string;
|
|
11
|
+
'auth.action.continueApple': string;
|
|
12
|
+
'auth.action.continueFacebook': string;
|
|
13
|
+
'auth.state.signedOut': string;
|
|
14
|
+
'auth.state.authenticating': string;
|
|
15
|
+
'auth.state.signedIn': string;
|
|
16
|
+
'auth.state.signingOut': string;
|
|
17
|
+
// Access tier (user-facing)
|
|
18
|
+
'auth.tier.free': string;
|
|
19
|
+
'auth.tier.premium': string;
|
|
20
|
+
'auth.tier.unverified': string;
|
|
21
|
+
// User-actionable errors
|
|
22
|
+
'auth.error.cancelled': string;
|
|
23
|
+
'auth.error.sessionExpired': string;
|
|
24
|
+
'auth.error.entitlement': string;
|
|
25
|
+
'auth.error.network': string;
|
|
26
|
+
// Developer / tenant-configuration errors
|
|
27
|
+
'auth.error.configuration': string;
|
|
28
|
+
'auth.error.redirectMismatch': string;
|
|
29
|
+
'auth.error.token': string;
|
|
30
|
+
'auth.error.consentDenied': string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type AuthMessageKey = keyof AuthMessageMap;
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_MESSAGES: AuthMessageMap = {
|
|
36
|
+
'auth.action.continueGoogle': 'Continue with Google',
|
|
37
|
+
'auth.action.continueApple': 'Continue with Apple',
|
|
38
|
+
'auth.action.continueFacebook': 'Continue with Facebook',
|
|
39
|
+
'auth.state.signedOut': 'Sign in to continue',
|
|
40
|
+
'auth.state.authenticating': 'Signing you in…',
|
|
41
|
+
'auth.state.signedIn': 'You are signed in',
|
|
42
|
+
'auth.state.signingOut': 'Signing out…',
|
|
43
|
+
'auth.tier.free': 'Free',
|
|
44
|
+
'auth.tier.premium': 'Premium',
|
|
45
|
+
'auth.tier.unverified': 'Membership could not be verified',
|
|
46
|
+
'auth.error.cancelled': 'Sign-in was cancelled. You can try again.',
|
|
47
|
+
'auth.error.sessionExpired': 'Your session expired. Please sign in again.',
|
|
48
|
+
'auth.error.entitlement': 'This feature requires a verified premium membership.',
|
|
49
|
+
'auth.error.network': 'No connection. Some features are limited until you reconnect.',
|
|
50
|
+
'auth.error.configuration': 'Authentication is not configured correctly.',
|
|
51
|
+
'auth.error.redirectMismatch': 'The sign-in redirect does not match the app configuration.',
|
|
52
|
+
'auth.error.token': 'We could not complete the request. Please try again.',
|
|
53
|
+
'auth.error.consentDenied': 'Access to this resource was denied.',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Resolve a message, preferring app overrides over the English defaults. */
|
|
57
|
+
export function resolveMessages(overrides?: Partial<AuthMessageMap>): AuthMessageMap {
|
|
58
|
+
return { ...DEFAULT_MESSAGES, ...(overrides ?? {}) };
|
|
59
|
+
}
|