@djangocfg/api 2.1.582 → 2.1.584
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/README.md +18 -0
- package/dist/OAuthTokenResponse-WEDM6IP5.mjs +19 -0
- package/dist/OAuthTokenResponse-WEDM6IP5.mjs.map +1 -0
- package/dist/OTPVerifyResponse-P5LDNM7H.mjs +20 -0
- package/dist/OTPVerifyResponse-P5LDNM7H.mjs.map +1 -0
- package/dist/PasswordLoginResponse-33B576NG.mjs +20 -0
- package/dist/PasswordLoginResponse-33B576NG.mjs.map +1 -0
- package/dist/auth.cjs +279 -103
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +14 -41
- package/dist/auth.d.ts +14 -41
- package/dist/auth.mjs +279 -101
- package/dist/auth.mjs.map +1 -1
- package/dist/clients.cjs +242 -54
- package/dist/clients.cjs.map +1 -1
- package/dist/clients.d.cts +13 -41
- package/dist/clients.d.ts +13 -41
- package/dist/clients.mjs +242 -54
- package/dist/clients.mjs.map +1 -1
- package/dist/hooks.cjs +242 -54
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.mjs +242 -54
- package/dist/hooks.mjs.map +1 -1
- package/dist/index.cjs +247 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -63
- package/dist/index.d.ts +90 -63
- package/dist/index.mjs +247 -54
- package/dist/index.mjs.map +1 -1
- package/dist/storage.cjs +268 -0
- package/dist/storage.cjs.map +1 -0
- package/dist/storage.d.cts +84 -0
- package/dist/storage.d.ts +84 -0
- package/dist/storage.mjs +247 -0
- package/dist/storage.mjs.map +1 -0
- package/package.json +8 -2
- package/src/__tests__/noStorageLiterals.test.ts +27 -0
- package/src/__tests__/storage.test.ts +134 -0
- package/src/__tests__/storageSsr.test.ts +14 -0
- package/src/_api/generated/_cfg_accounts/openapi.json +1 -28
- package/src/_api/generated/_cfg_accounts/schemas/OAuthCallbackRequestRequest.ts +0 -1
- package/src/_api/generated/_cfg_accounts/schemas/OAuthTokenResponse.ts +0 -1
- package/src/_api/generated/_cfg_accounts/schemas/OTPVerifyRequest.ts +0 -1
- package/src/_api/generated/_cfg_accounts/schemas/OTPVerifyResponse.ts +0 -1
- package/src/_api/generated/_cfg_accounts/schemas/PasswordLoginRequest.ts +0 -1
- package/src/_api/generated/_cfg_accounts/schemas/PasswordLoginResponse.ts +0 -1
- package/src/_api/generated/_cfg_totp/openapi.json +0 -5
- package/src/_api/generated/_cfg_totp/schemas/VerifyResponse.ts +0 -1
- package/src/_api/generated/helpers/auth.ts +28 -65
- package/src/_api/generated/helpers/index.ts +12 -0
- package/src/_api/generated/helpers/keys.ts +327 -0
- package/src/_api/generated/openapi.json +1 -33
- package/src/_api/generated/types.gen.ts +4 -59
- package/src/auth/__tests__/authRedirect.test.ts +1 -1
- package/src/auth/__tests__/passwordLogin.test.tsx +1 -3
- package/src/auth/__tests__/profileCache.test.ts +1 -1
- package/src/auth/context/AccountsContext.tsx +6 -8
- package/src/auth/context/AuthContext.tsx +3 -5
- package/src/auth/context/types.ts +1 -1
- package/src/auth/hooks/useAuthForm.ts +3 -5
- package/src/auth/hooks/useAuthFormState.ts +0 -4
- package/src/auth/hooks/useAuthRedirect.ts +3 -2
- package/src/auth/hooks/useGithubAuth.ts +14 -16
- package/src/auth/hooks/useLocalStorage.ts +7 -7
- package/src/auth/hooks/useProfileCache.ts +2 -1
- package/src/auth/hooks/useSessionStorage.ts +7 -7
- package/src/auth/hooks/useTwoFactorVerify.ts +2 -2
- package/src/auth/types/form.ts +0 -5
- package/src/storage.ts +21 -0
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
// DO NOT EDIT — re-run `make gen`.
|
|
6
6
|
|
|
7
7
|
import { APIError } from './errors';
|
|
8
|
+
import { cfgStorage, createStorage, type KeyStore } from './keys';
|
|
8
9
|
|
|
9
|
-
const ACCESS_KEY = 'cfg.access_token';
|
|
10
|
-
const REFRESH_KEY = 'cfg.refresh_token';
|
|
11
|
-
const API_KEY_KEY = 'cfg.api_key';
|
|
10
|
+
const ACCESS_KEY = 'cfg.auth.access_token';
|
|
11
|
+
const REFRESH_KEY = 'cfg.auth.refresh_token';
|
|
12
|
+
const API_KEY_KEY = 'cfg.auth.api_key';
|
|
13
|
+
|
|
14
|
+
// The framework's own keys use the shared registry; a custom owner gets the same guarded helpers.
|
|
15
|
+
const storage = 'cfg' === 'cfg' ? cfgStorage : createStorage('cfg');
|
|
12
16
|
|
|
13
17
|
const isBrowser = typeof window !== 'undefined';
|
|
14
18
|
|
|
@@ -21,68 +25,24 @@ interface KVStore {
|
|
|
21
25
|
set(key: string, value: string | null): void;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
if (value === null) window.localStorage.removeItem(key);
|
|
33
|
-
else window.localStorage.setItem(key, value);
|
|
34
|
-
} catch {}
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const sessionStorageBackend: KVStore = {
|
|
39
|
-
get(key) {
|
|
40
|
-
if (!isBrowser) return null;
|
|
41
|
-
try { return window.sessionStorage.getItem(key); } catch { return null; }
|
|
42
|
-
},
|
|
43
|
-
set(key, value) {
|
|
44
|
-
if (!isBrowser) return;
|
|
45
|
-
try {
|
|
46
|
-
if (value === null) window.sessionStorage.removeItem(key);
|
|
47
|
-
else window.sessionStorage.setItem(key, value);
|
|
48
|
-
} catch {}
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/** 30 days, matches typical refresh-token lifetime. */
|
|
53
|
-
const COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
|
|
54
|
-
|
|
55
|
-
const cookieBackend: KVStore = {
|
|
56
|
-
get(key) {
|
|
57
|
-
if (!isBrowser) return null;
|
|
58
|
-
try {
|
|
59
|
-
const re = new RegExp(`(?:^|;\\s*)${encodeURIComponent(key)}=([^;]*)`);
|
|
60
|
-
const m = document.cookie.match(re);
|
|
61
|
-
const value = m?.[1];
|
|
62
|
-
return value === undefined ? null : decodeURIComponent(value);
|
|
63
|
-
} catch { return null; }
|
|
64
|
-
},
|
|
65
|
-
set(key, value) {
|
|
66
|
-
if (!isBrowser) return;
|
|
67
|
-
try {
|
|
68
|
-
const k = encodeURIComponent(key);
|
|
69
|
-
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
|
|
70
|
-
if (value === null) {
|
|
71
|
-
document.cookie = `${k}=; Path=/; Max-Age=0; SameSite=Lax${secure}`;
|
|
72
|
-
} else {
|
|
73
|
-
const v = encodeURIComponent(value);
|
|
74
|
-
document.cookie = `${k}=${v}; Path=/; Max-Age=${COOKIE_MAX_AGE}; SameSite=Lax${secure}`;
|
|
75
|
-
}
|
|
76
|
-
} catch {}
|
|
77
|
-
},
|
|
28
|
+
// Every read and write goes through the registry's guarded storage: SSR-safe,
|
|
29
|
+
// private-mode and quota safe, and a key written under an older spelling is
|
|
30
|
+
// moved to its `cfg.auth.*` name on first read.
|
|
31
|
+
const STORE_OF: Record<StorageMode, KeyStore> = {
|
|
32
|
+
localStorage: 'local',
|
|
33
|
+
sessionStorage: 'session',
|
|
34
|
+
cookie: 'cookie',
|
|
78
35
|
};
|
|
79
36
|
|
|
80
37
|
function backendFor(mode: StorageMode): KVStore {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
:
|
|
84
|
-
|
|
85
|
-
|
|
38
|
+
const store = STORE_OF[mode];
|
|
39
|
+
return {
|
|
40
|
+
get: (key) => storage.read(key, store),
|
|
41
|
+
set(key, value) {
|
|
42
|
+
if (value === null) storage.remove(key, store);
|
|
43
|
+
else storage.write(key, value, store);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
86
46
|
}
|
|
87
47
|
|
|
88
48
|
/**
|
|
@@ -96,7 +56,7 @@ function backendFor(mode: StorageMode): KVStore {
|
|
|
96
56
|
* to the client, and every request would go out unauthenticated.
|
|
97
57
|
*
|
|
98
58
|
* Session LENGTH is therefore not a client concern. The server decides it when
|
|
99
|
-
* minting the refresh token
|
|
59
|
+
* minting the refresh token; the client just stores whatever it
|
|
100
60
|
* was given, in one place.
|
|
101
61
|
*/
|
|
102
62
|
let _storageMode: StorageMode = 'localStorage';
|
|
@@ -411,10 +371,13 @@ export const auth = {
|
|
|
411
371
|
// Clear every backend, not just the active one. Sign-out must leave nothing
|
|
412
372
|
// behind anywhere — including tokens an older build stranded in
|
|
413
373
|
// sessionStorage — or the next page load could resurrect a dead session.
|
|
414
|
-
for (const
|
|
374
|
+
for (const mode of ['localStorage', 'sessionStorage', 'cookie'] as const) {
|
|
375
|
+
const backend = backendFor(mode);
|
|
415
376
|
backend.set(ACCESS_KEY, null);
|
|
416
377
|
backend.set(REFRESH_KEY, null);
|
|
417
378
|
}
|
|
379
|
+
// The rest of the signed-in user's keys (profile cache, OAuth leftovers) go with the tokens.
|
|
380
|
+
storage.clearAll();
|
|
418
381
|
notifySessionChanged();
|
|
419
382
|
},
|
|
420
383
|
/** Session-aware: token PRESENT and not past its `exp` (or a live refresh
|
|
@@ -679,7 +642,7 @@ function dpopEnabled(): boolean {
|
|
|
679
642
|
} catch { return false; }
|
|
680
643
|
}
|
|
681
644
|
|
|
682
|
-
const _DPOP_DB = 'cfg
|
|
645
|
+
const _DPOP_DB = 'cfg.auth.dpop';
|
|
683
646
|
const _DPOP_STORE = 'keys';
|
|
684
647
|
const _DPOP_KEY_ID = 'dpop-ec-p256';
|
|
685
648
|
|
|
@@ -9,6 +9,18 @@ export {
|
|
|
9
9
|
CookieStorageAdapter,
|
|
10
10
|
} from './storage';
|
|
11
11
|
export { APIError, NetworkError } from './errors';
|
|
12
|
+
export {
|
|
13
|
+
CFG_KEYS,
|
|
14
|
+
CFG_FAMILIES,
|
|
15
|
+
cfgStorage,
|
|
16
|
+
createStorage,
|
|
17
|
+
normalizeSegment,
|
|
18
|
+
type KeySpec,
|
|
19
|
+
type FamilySpec,
|
|
20
|
+
type KeyStore,
|
|
21
|
+
type GuardedStorage,
|
|
22
|
+
type StorageOptions,
|
|
23
|
+
} from './keys';
|
|
12
24
|
export {
|
|
13
25
|
APILogger,
|
|
14
26
|
defaultLogger,
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// AUTO-GENERATED by django_generator / ts_extras.wrapper
|
|
2
|
+
// Browser-storage key registry: one scheme, guarded access.
|
|
3
|
+
// DO NOT EDIT — re-run `make gen`.
|
|
4
|
+
//
|
|
5
|
+
// Rules for agents and apps: @docs/STORAGE_KEYS.md in @djangocfg/api.
|
|
6
|
+
//
|
|
7
|
+
// Scheme: `<owner>.<area>.<name>` — lower case, dots between levels,
|
|
8
|
+
// `snake_case` inside a level. The framework's owner is `cfg`; an app picks its
|
|
9
|
+
// own with `createStorage('<owner>')`. Third-party keys (a library's own
|
|
10
|
+
// prefix, `NEXT_LOCALE`) are not ours to rename and stay as they are.
|
|
11
|
+
|
|
12
|
+
export type KeyStore = 'local' | 'session' | 'cookie';
|
|
13
|
+
|
|
14
|
+
export interface KeySpec {
|
|
15
|
+
/** The canonical key. */
|
|
16
|
+
readonly key: string;
|
|
17
|
+
readonly store: KeyStore;
|
|
18
|
+
/** Removed by `clearAll()` (sign-out). Preferences are kept. */
|
|
19
|
+
readonly personal: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FamilySpec {
|
|
23
|
+
/** Canonical prefix ending in a dot; the id is appended in normalised form. */
|
|
24
|
+
readonly prefix: string;
|
|
25
|
+
readonly store: 'local' | 'session';
|
|
26
|
+
readonly personal: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const k = (
|
|
30
|
+
key: string,
|
|
31
|
+
store: KeyStore,
|
|
32
|
+
personal: boolean,
|
|
33
|
+
): KeySpec => ({ key, store, personal });
|
|
34
|
+
|
|
35
|
+
const f = (
|
|
36
|
+
prefix: string,
|
|
37
|
+
store: 'local' | 'session',
|
|
38
|
+
personal: boolean,
|
|
39
|
+
): FamilySpec => ({ prefix, store, personal });
|
|
40
|
+
|
|
41
|
+
/** Every key the framework writes. No legacy spellings are read: old keys are simply ignored. */
|
|
42
|
+
export const CFG_KEYS = {
|
|
43
|
+
accessToken: k('cfg.auth.access_token', 'local', true),
|
|
44
|
+
refreshToken: k('cfg.auth.refresh_token', 'local', true),
|
|
45
|
+
apiKey: k('cfg.auth.api_key', 'local', true),
|
|
46
|
+
email: k('cfg.auth.email', 'local', false),
|
|
47
|
+
emailTs: k('cfg.auth.email_ts', 'local', false),
|
|
48
|
+
redirectUrl: k('cfg.auth.redirect_url', 'session', true),
|
|
49
|
+
redirectUrlTs: k('cfg.auth.redirect_url_ts', 'session', true),
|
|
50
|
+
oauthState: k('cfg.auth.oauth_state', 'session', true),
|
|
51
|
+
oauthProvider: k('cfg.auth.oauth_provider', 'session', true),
|
|
52
|
+
profileCache: k('cfg.profile.cache', 'local', true),
|
|
53
|
+
monitorSessionId: k('cfg.monitor.session_id', 'local', false),
|
|
54
|
+
monitorSessionCookie: k('cfg.monitor.session_id', 'cookie', false),
|
|
55
|
+
monitorDebug: k('cfg.monitor.debug', 'local', false),
|
|
56
|
+
uiTabId: k('cfg.ui.tab_id', 'session', false),
|
|
57
|
+
uiRouterSerial: k('cfg.ui.router_serial', 'session', false),
|
|
58
|
+
uiState: k('cfg.ui.state', 'local', false),
|
|
59
|
+
chatSpeechLanguage: k('cfg.chat.speech_language', 'local', false),
|
|
60
|
+
chatRecentTools: k('cfg.chat.recent_tools', 'local', false),
|
|
61
|
+
formsSpeechDebug: k('cfg.forms.speech_debug', 'local', false),
|
|
62
|
+
formsSpeechPrefs: k('cfg.forms.speech_prefs', 'local', false),
|
|
63
|
+
agentPlaygroundApiKeyId: k('cfg.agent.playground_api_key_id', 'session', true),
|
|
64
|
+
agentPlaygroundBearer: k('cfg.agent.playground_bearer', 'session', true),
|
|
65
|
+
agentEndpointDoc: k('cfg.agent.endpoint_doc', 'session', false),
|
|
66
|
+
} as const satisfies Record<string, KeySpec>;
|
|
67
|
+
|
|
68
|
+
/** Keys with a caller-supplied id part: one per room, tree, endpoint. */
|
|
69
|
+
export const CFG_FAMILIES = {
|
|
70
|
+
chatDraft: f('cfg.chat.draft.', 'session', true),
|
|
71
|
+
dataTree: f('cfg.data.tree.', 'local', false),
|
|
72
|
+
agentEndpointDraft: f('cfg.agent.endpoint_draft.', 'local', false),
|
|
73
|
+
} as const satisfies Record<string, FamilySpec>;
|
|
74
|
+
|
|
75
|
+
const OWNER_RE = /^[a-z][a-z0-9_]*$/;
|
|
76
|
+
|
|
77
|
+
/** One segment of a key: lower case, `[a-z0-9_]`, anything else folded to `_`. */
|
|
78
|
+
export function normalizeSegment(raw: string): string {
|
|
79
|
+
return raw
|
|
80
|
+
.trim()
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.replace(/[^a-z0-9_]+/g, '_')
|
|
83
|
+
.replace(/^_+|_+$/g, '');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface StorageOptions {
|
|
87
|
+
/** Named keys of this owner. */
|
|
88
|
+
readonly keys?: Readonly<Record<string, KeySpec>>;
|
|
89
|
+
readonly families?: Readonly<Record<string, FamilySpec>>;
|
|
90
|
+
/** Cookie lifetime for the `cookie` store. Default 30 days. */
|
|
91
|
+
readonly cookieMaxAgeSeconds?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface GuardedStorage {
|
|
95
|
+
readonly owner: string;
|
|
96
|
+
/** Builds `<owner>.<part>.<part>` from free-form parts; throws if a part is empty. */
|
|
97
|
+
key(...parts: string[]): string;
|
|
98
|
+
/** Builds a family member key: the prefix plus the normalised id. */
|
|
99
|
+
familyKey(family: FamilySpec, id: string): string;
|
|
100
|
+
/** `^(cfg|<owner>)(\.[a-z0-9_]+)+$`. */
|
|
101
|
+
isValid(key: string): boolean;
|
|
102
|
+
read(key: string, store?: KeyStore): string | null;
|
|
103
|
+
/** True when the value reached the browser's storage; false means it is held in memory for this page only. */
|
|
104
|
+
write(key: string, value: string, store?: KeyStore): boolean;
|
|
105
|
+
/** Removes the key. */
|
|
106
|
+
remove(key: string, store?: KeyStore): void;
|
|
107
|
+
readJSON<T>(key: string, store?: KeyStore): T | null;
|
|
108
|
+
writeJSON(key: string, value: unknown, store?: KeyStore): boolean;
|
|
109
|
+
/** Keys this owner holds in `store`. */
|
|
110
|
+
keys(store?: Exclude<KeyStore, 'cookie'>): string[];
|
|
111
|
+
/** Sign-out: removes the `personal` keys. `{ all: true }` removes everything under this owner. */
|
|
112
|
+
clearAll(options?: { all?: boolean }): void;
|
|
113
|
+
/** Cross-tab: calls `listener` with the new value when ANOTHER tab changes `key`. Cookies fire no events. */
|
|
114
|
+
onChange(key: string, listener: (value: string | null) => void, store?: Exclude<KeyStore, 'cookie'>): () => void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const hasWindow = () => typeof window !== 'undefined';
|
|
118
|
+
|
|
119
|
+
function nativeStorage(store: 'local' | 'session'): Storage | null {
|
|
120
|
+
if (!hasWindow()) return null;
|
|
121
|
+
try {
|
|
122
|
+
return store === 'session' ? window.sessionStorage : window.localStorage;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function cookieNames(): string[] {
|
|
129
|
+
if (!hasWindow()) return [];
|
|
130
|
+
try {
|
|
131
|
+
return document.cookie
|
|
132
|
+
.split('; ')
|
|
133
|
+
.map((part) => decodeURIComponent(part.split('=')[0] ?? ''))
|
|
134
|
+
.filter(Boolean);
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function createStorage(owner: string, options: StorageOptions = {}): GuardedStorage {
|
|
141
|
+
if (!OWNER_RE.test(owner)) {
|
|
142
|
+
throw new TypeError(`createStorage: owner "${owner}" must match ${OWNER_RE}`);
|
|
143
|
+
}
|
|
144
|
+
const prefix = `${owner}.`;
|
|
145
|
+
const pattern = owner === 'cfg' ? /^cfg(\.[a-z0-9_]+)+$/ : new RegExp(`^(cfg|${owner})(\\.[a-z0-9_]+)+$`);
|
|
146
|
+
const cookieMaxAge = options.cookieMaxAgeSeconds ?? 60 * 60 * 24 * 30;
|
|
147
|
+
const specs = Object.values(options.keys ?? {});
|
|
148
|
+
const families = Object.values(options.families ?? {});
|
|
149
|
+
const specByKey = new Map<string, KeySpec[]>();
|
|
150
|
+
for (const spec of specs) specByKey.set(spec.key, [...(specByKey.get(spec.key) ?? []), spec]);
|
|
151
|
+
// Held in memory when the browser refuses a write (quota, private mode), so the page still works until reload.
|
|
152
|
+
const memory: Record<KeyStore, Map<string, string>> = {
|
|
153
|
+
local: new Map(),
|
|
154
|
+
session: new Map(),
|
|
155
|
+
cookie: new Map(),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const isValid = (key: string) => pattern.test(key);
|
|
159
|
+
const assertValid = (key: string) => {
|
|
160
|
+
if (!isValid(key)) throw new TypeError(`storage key "${key}" must match ${pattern}`);
|
|
161
|
+
};
|
|
162
|
+
const assertOwned = (key: string) => {
|
|
163
|
+
assertValid(key);
|
|
164
|
+
if (!key.startsWith(prefix)) throw new TypeError(`storage key "${key}" is not owned by "${owner}"`);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
function rawGet(store: KeyStore, key: string): string | null {
|
|
168
|
+
if (!hasWindow()) return null;
|
|
169
|
+
if (store === 'cookie') {
|
|
170
|
+
try {
|
|
171
|
+
const escaped = encodeURIComponent(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
172
|
+
const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]*)`));
|
|
173
|
+
if (match?.[1] !== undefined) return decodeURIComponent(match[1]);
|
|
174
|
+
} catch {
|
|
175
|
+
/* fall through to memory */
|
|
176
|
+
}
|
|
177
|
+
return memory.cookie.get(key) ?? null;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const value = nativeStorage(store)?.getItem(key) ?? null;
|
|
181
|
+
if (value !== null) return value;
|
|
182
|
+
} catch {
|
|
183
|
+
/* fall through to memory */
|
|
184
|
+
}
|
|
185
|
+
return memory[store].get(key) ?? null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function rawSet(store: KeyStore, key: string, value: string): boolean {
|
|
189
|
+
if (!hasWindow()) return false;
|
|
190
|
+
try {
|
|
191
|
+
if (store === 'cookie') {
|
|
192
|
+
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
|
|
193
|
+
document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(value)}; Path=/; Max-Age=${cookieMaxAge}; SameSite=Lax${secure}`;
|
|
194
|
+
if (rawGet('cookie', key) === value && !memory.cookie.has(key)) return true;
|
|
195
|
+
memory.cookie.set(key, value);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const native = nativeStorage(store);
|
|
199
|
+
if (!native) throw new Error('storage unavailable');
|
|
200
|
+
native.setItem(key, value);
|
|
201
|
+
memory[store].delete(key);
|
|
202
|
+
return true;
|
|
203
|
+
} catch {
|
|
204
|
+
memory[store].set(key, value);
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function rawRemove(store: KeyStore, key: string): void {
|
|
210
|
+
memory[store].delete(key);
|
|
211
|
+
if (!hasWindow()) return;
|
|
212
|
+
try {
|
|
213
|
+
if (store === 'cookie') {
|
|
214
|
+
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
|
|
215
|
+
document.cookie = `${encodeURIComponent(key)}=; Path=/; Max-Age=0; SameSite=Lax${secure}`;
|
|
216
|
+
} else {
|
|
217
|
+
nativeStorage(store)?.removeItem(key);
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
/* ignore */
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function resolveStore(key: string, store?: KeyStore): KeyStore {
|
|
225
|
+
return store ?? specByKey.get(key)?.[0]?.store ?? 'local';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function listKeys(store: 'local' | 'session', startsWith: string): string[] {
|
|
229
|
+
const out: string[] = [];
|
|
230
|
+
try {
|
|
231
|
+
const native = nativeStorage(store);
|
|
232
|
+
if (native) {
|
|
233
|
+
for (let i = 0; i < native.length; i += 1) {
|
|
234
|
+
const name = native.key(i);
|
|
235
|
+
if (name?.startsWith(startsWith)) out.push(name);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
/* ignore */
|
|
240
|
+
}
|
|
241
|
+
for (const name of memory[store].keys()) if (name.startsWith(startsWith) && !out.includes(name)) out.push(name);
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const familyKey = (family: FamilySpec, id: string) => {
|
|
246
|
+
const part = normalizeSegment(id);
|
|
247
|
+
if (!part) throw new TypeError('storage family id is empty after normalisation');
|
|
248
|
+
return family.prefix + part;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const api: GuardedStorage = {
|
|
252
|
+
owner,
|
|
253
|
+
key(...parts) {
|
|
254
|
+
const segments = parts.map(normalizeSegment);
|
|
255
|
+
if (segments.length === 0 || segments.some((segment) => !segment)) {
|
|
256
|
+
throw new TypeError(`storage key parts must be non-empty: ${JSON.stringify(parts)}`);
|
|
257
|
+
}
|
|
258
|
+
return `${prefix}${segments.join('.')}`;
|
|
259
|
+
},
|
|
260
|
+
familyKey,
|
|
261
|
+
isValid,
|
|
262
|
+
read(key, store) {
|
|
263
|
+
assertValid(key);
|
|
264
|
+
const target = resolveStore(key, store);
|
|
265
|
+
return rawGet(target, key);
|
|
266
|
+
},
|
|
267
|
+
write(key, value, store) {
|
|
268
|
+
assertOwned(key);
|
|
269
|
+
const target = resolveStore(key, store);
|
|
270
|
+
return rawSet(target, key, value);
|
|
271
|
+
},
|
|
272
|
+
remove(key, store) {
|
|
273
|
+
assertOwned(key);
|
|
274
|
+
const target = resolveStore(key, store);
|
|
275
|
+
rawRemove(target, key);
|
|
276
|
+
},
|
|
277
|
+
readJSON<T>(key: string, store?: KeyStore): T | null {
|
|
278
|
+
const raw = api.read(key, store);
|
|
279
|
+
if (raw === null) return null;
|
|
280
|
+
try {
|
|
281
|
+
return JSON.parse(raw) as T;
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
writeJSON(key, value, store) {
|
|
287
|
+
return api.write(key, JSON.stringify(value), store);
|
|
288
|
+
},
|
|
289
|
+
keys(store = 'local') {
|
|
290
|
+
return listKeys(store, prefix);
|
|
291
|
+
},
|
|
292
|
+
clearAll({ all = false } = {}) {
|
|
293
|
+
for (const spec of specs) {
|
|
294
|
+
if (!all && !spec.personal) continue;
|
|
295
|
+
rawRemove(spec.store, spec.key);
|
|
296
|
+
}
|
|
297
|
+
for (const family of families) {
|
|
298
|
+
if (!all && !family.personal) continue;
|
|
299
|
+
for (const name of listKeys(family.store, family.prefix)) rawRemove(family.store, name);
|
|
300
|
+
}
|
|
301
|
+
if (all) {
|
|
302
|
+
for (const store of ['local', 'session'] as const) {
|
|
303
|
+
for (const name of listKeys(store, prefix)) rawRemove(store, name);
|
|
304
|
+
}
|
|
305
|
+
for (const name of cookieNames()) if (name.startsWith(prefix)) rawRemove('cookie', name);
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
onChange(key, listener, store = 'local') {
|
|
309
|
+
assertValid(key);
|
|
310
|
+
if (!hasWindow()) return () => {};
|
|
311
|
+
const handler = (event: StorageEvent) => {
|
|
312
|
+
if (event.storageArea !== nativeStorage(store)) return;
|
|
313
|
+
if (event.key === null || event.key === key) listener(event.key === null ? null : event.newValue);
|
|
314
|
+
};
|
|
315
|
+
window.addEventListener('storage', handler);
|
|
316
|
+
return () => window.removeEventListener('storage', handler);
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
return api;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** The framework's own storage: every `cfg.*` key. */
|
|
323
|
+
export const cfgStorage: GuardedStorage = createStorage('cfg', {
|
|
324
|
+
keys: CFG_KEYS,
|
|
325
|
+
families: CFG_FAMILIES,
|
|
326
|
+
});
|
|
327
|
+
|
|
@@ -2098,11 +2098,6 @@
|
|
|
2098
2098
|
"type": "string",
|
|
2099
2099
|
"format": "uri",
|
|
2100
2100
|
"description": "Same redirect_uri used in authorize request. If not provided, uses config's site_url + callback_path"
|
|
2101
|
-
},
|
|
2102
|
-
"remember_me": {
|
|
2103
|
-
"type": "boolean",
|
|
2104
|
-
"default": false,
|
|
2105
|
-
"description": "Keep this browser signed in for 30 days"
|
|
2106
2101
|
}
|
|
2107
2102
|
},
|
|
2108
2103
|
"required": [
|
|
@@ -2282,10 +2277,6 @@
|
|
|
2282
2277
|
"should_prompt_2fa": {
|
|
2283
2278
|
"type": "boolean",
|
|
2284
2279
|
"description": "True if user should be prompted to enable 2FA"
|
|
2285
|
-
},
|
|
2286
|
-
"persistent_session": {
|
|
2287
|
-
"type": "boolean",
|
|
2288
|
-
"description": "Whether this login should persist across browser restarts"
|
|
2289
2280
|
}
|
|
2290
2281
|
}
|
|
2291
2282
|
},
|
|
@@ -2399,11 +2390,6 @@
|
|
|
2399
2390
|
"type": "string",
|
|
2400
2391
|
"format": "uri",
|
|
2401
2392
|
"description": "Source URL for tracking login (e.g., https://my.djangocfg.com)"
|
|
2402
|
-
},
|
|
2403
|
-
"remember_me": {
|
|
2404
|
-
"type": "boolean",
|
|
2405
|
-
"default": false,
|
|
2406
|
-
"description": "Keep this browser signed in for 30 days"
|
|
2407
2393
|
}
|
|
2408
2394
|
},
|
|
2409
2395
|
"required": [
|
|
@@ -2456,10 +2442,6 @@
|
|
|
2456
2442
|
"should_prompt_2fa": {
|
|
2457
2443
|
"type": "boolean",
|
|
2458
2444
|
"description": "Whether user should be prompted to enable 2FA"
|
|
2459
|
-
},
|
|
2460
|
-
"persistent_session": {
|
|
2461
|
-
"type": "boolean",
|
|
2462
|
-
"description": "Whether this login should persist across browser restarts"
|
|
2463
2445
|
}
|
|
2464
2446
|
}
|
|
2465
2447
|
},
|
|
@@ -2490,13 +2472,8 @@
|
|
|
2490
2472
|
},
|
|
2491
2473
|
"PasswordLoginRequest": {
|
|
2492
2474
|
"type": "object",
|
|
2493
|
-
"description": "Validate email+password against live accounts only.\n\nSubclasses SimpleJWT so the field/validation contract stays theirs, but\nreplaces ``validate()`` — the token pair itself is minted by the view via\n``mint_tokens_for_request`` so the DPoP binding and
|
|
2475
|
+
"description": "Validate email+password against live accounts only.\n\nSubclasses SimpleJWT so the field/validation contract stays theirs, but\nreplaces ``validate()`` — the token pair itself is minted by the view via\n``mint_tokens_for_request`` so the DPoP binding and token\nlifetime match OTP, OAuth and TOTP exactly.",
|
|
2494
2476
|
"properties": {
|
|
2495
|
-
"remember_me": {
|
|
2496
|
-
"type": "boolean",
|
|
2497
|
-
"default": false,
|
|
2498
|
-
"description": "Keep this browser signed in for 30 days"
|
|
2499
|
-
},
|
|
2500
2477
|
"email": {
|
|
2501
2478
|
"type": "string",
|
|
2502
2479
|
"format": "email",
|
|
@@ -2558,10 +2535,6 @@
|
|
|
2558
2535
|
"should_prompt_2fa": {
|
|
2559
2536
|
"type": "boolean",
|
|
2560
2537
|
"description": "Whether user should be prompted to enable 2FA"
|
|
2561
|
-
},
|
|
2562
|
-
"persistent_session": {
|
|
2563
|
-
"type": "boolean",
|
|
2564
|
-
"description": "Whether this login should persist across browser restarts"
|
|
2565
2538
|
}
|
|
2566
2539
|
}
|
|
2567
2540
|
},
|
|
@@ -3029,10 +3002,6 @@
|
|
|
3029
3002
|
],
|
|
3030
3003
|
"description": "User profile data"
|
|
3031
3004
|
},
|
|
3032
|
-
"persistent_session": {
|
|
3033
|
-
"type": "boolean",
|
|
3034
|
-
"description": "Whether this login should persist across browser restarts"
|
|
3035
|
-
},
|
|
3036
3005
|
"remaining_backup_codes": {
|
|
3037
3006
|
"type": "integer",
|
|
3038
3007
|
"description": "Number of remaining backup codes (if backup code was used)"
|
|
@@ -3045,7 +3014,6 @@
|
|
|
3045
3014
|
"required": [
|
|
3046
3015
|
"access_token",
|
|
3047
3016
|
"message",
|
|
3048
|
-
"persistent_session",
|
|
3049
3017
|
"refresh_token",
|
|
3050
3018
|
"user"
|
|
3051
3019
|
]
|