@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
package/dist/storage.mjs
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/_api/generated/helpers/keys.ts
|
|
5
|
+
var k = /* @__PURE__ */ __name((key, store, personal) => ({ key, store, personal }), "k");
|
|
6
|
+
var f = /* @__PURE__ */ __name((prefix, store, personal) => ({ prefix, store, personal }), "f");
|
|
7
|
+
var CFG_KEYS = {
|
|
8
|
+
accessToken: k("cfg.auth.access_token", "local", true),
|
|
9
|
+
refreshToken: k("cfg.auth.refresh_token", "local", true),
|
|
10
|
+
apiKey: k("cfg.auth.api_key", "local", true),
|
|
11
|
+
email: k("cfg.auth.email", "local", false),
|
|
12
|
+
emailTs: k("cfg.auth.email_ts", "local", false),
|
|
13
|
+
redirectUrl: k("cfg.auth.redirect_url", "session", true),
|
|
14
|
+
redirectUrlTs: k("cfg.auth.redirect_url_ts", "session", true),
|
|
15
|
+
oauthState: k("cfg.auth.oauth_state", "session", true),
|
|
16
|
+
oauthProvider: k("cfg.auth.oauth_provider", "session", true),
|
|
17
|
+
profileCache: k("cfg.profile.cache", "local", true),
|
|
18
|
+
monitorSessionId: k("cfg.monitor.session_id", "local", false),
|
|
19
|
+
monitorSessionCookie: k("cfg.monitor.session_id", "cookie", false),
|
|
20
|
+
monitorDebug: k("cfg.monitor.debug", "local", false),
|
|
21
|
+
uiTabId: k("cfg.ui.tab_id", "session", false),
|
|
22
|
+
uiRouterSerial: k("cfg.ui.router_serial", "session", false),
|
|
23
|
+
uiState: k("cfg.ui.state", "local", false),
|
|
24
|
+
chatSpeechLanguage: k("cfg.chat.speech_language", "local", false),
|
|
25
|
+
chatRecentTools: k("cfg.chat.recent_tools", "local", false),
|
|
26
|
+
formsSpeechDebug: k("cfg.forms.speech_debug", "local", false),
|
|
27
|
+
formsSpeechPrefs: k("cfg.forms.speech_prefs", "local", false),
|
|
28
|
+
agentPlaygroundApiKeyId: k("cfg.agent.playground_api_key_id", "session", true),
|
|
29
|
+
agentPlaygroundBearer: k("cfg.agent.playground_bearer", "session", true),
|
|
30
|
+
agentEndpointDoc: k("cfg.agent.endpoint_doc", "session", false)
|
|
31
|
+
};
|
|
32
|
+
var CFG_FAMILIES = {
|
|
33
|
+
chatDraft: f("cfg.chat.draft.", "session", true),
|
|
34
|
+
dataTree: f("cfg.data.tree.", "local", false),
|
|
35
|
+
agentEndpointDraft: f("cfg.agent.endpoint_draft.", "local", false)
|
|
36
|
+
};
|
|
37
|
+
var OWNER_RE = /^[a-z][a-z0-9_]*$/;
|
|
38
|
+
function normalizeSegment(raw) {
|
|
39
|
+
return raw.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
|
|
40
|
+
}
|
|
41
|
+
__name(normalizeSegment, "normalizeSegment");
|
|
42
|
+
var hasWindow = /* @__PURE__ */ __name(() => typeof window !== "undefined", "hasWindow");
|
|
43
|
+
function nativeStorage(store) {
|
|
44
|
+
if (!hasWindow()) return null;
|
|
45
|
+
try {
|
|
46
|
+
return store === "session" ? window.sessionStorage : window.localStorage;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
__name(nativeStorage, "nativeStorage");
|
|
52
|
+
function cookieNames() {
|
|
53
|
+
if (!hasWindow()) return [];
|
|
54
|
+
try {
|
|
55
|
+
return document.cookie.split("; ").map((part) => decodeURIComponent(part.split("=")[0] ?? "")).filter(Boolean);
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
__name(cookieNames, "cookieNames");
|
|
61
|
+
function createStorage(owner, options = {}) {
|
|
62
|
+
if (!OWNER_RE.test(owner)) {
|
|
63
|
+
throw new TypeError(`createStorage: owner "${owner}" must match ${OWNER_RE}`);
|
|
64
|
+
}
|
|
65
|
+
const prefix = `${owner}.`;
|
|
66
|
+
const pattern = owner === "cfg" ? /^cfg(\.[a-z0-9_]+)+$/ : new RegExp(`^(cfg|${owner})(\\.[a-z0-9_]+)+$`);
|
|
67
|
+
const cookieMaxAge = options.cookieMaxAgeSeconds ?? 60 * 60 * 24 * 30;
|
|
68
|
+
const specs = Object.values(options.keys ?? {});
|
|
69
|
+
const families = Object.values(options.families ?? {});
|
|
70
|
+
const specByKey = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const spec of specs) specByKey.set(spec.key, [...specByKey.get(spec.key) ?? [], spec]);
|
|
72
|
+
const memory = {
|
|
73
|
+
local: /* @__PURE__ */ new Map(),
|
|
74
|
+
session: /* @__PURE__ */ new Map(),
|
|
75
|
+
cookie: /* @__PURE__ */ new Map()
|
|
76
|
+
};
|
|
77
|
+
const isValid = /* @__PURE__ */ __name((key) => pattern.test(key), "isValid");
|
|
78
|
+
const assertValid = /* @__PURE__ */ __name((key) => {
|
|
79
|
+
if (!isValid(key)) throw new TypeError(`storage key "${key}" must match ${pattern}`);
|
|
80
|
+
}, "assertValid");
|
|
81
|
+
const assertOwned = /* @__PURE__ */ __name((key) => {
|
|
82
|
+
assertValid(key);
|
|
83
|
+
if (!key.startsWith(prefix)) throw new TypeError(`storage key "${key}" is not owned by "${owner}"`);
|
|
84
|
+
}, "assertOwned");
|
|
85
|
+
function rawGet(store, key) {
|
|
86
|
+
if (!hasWindow()) return null;
|
|
87
|
+
if (store === "cookie") {
|
|
88
|
+
try {
|
|
89
|
+
const escaped = encodeURIComponent(key).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
90
|
+
const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]*)`));
|
|
91
|
+
if (match?.[1] !== void 0) return decodeURIComponent(match[1]);
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
return memory.cookie.get(key) ?? null;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const value = nativeStorage(store)?.getItem(key) ?? null;
|
|
98
|
+
if (value !== null) return value;
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
return memory[store].get(key) ?? null;
|
|
102
|
+
}
|
|
103
|
+
__name(rawGet, "rawGet");
|
|
104
|
+
function rawSet(store, key, value) {
|
|
105
|
+
if (!hasWindow()) return false;
|
|
106
|
+
try {
|
|
107
|
+
if (store === "cookie") {
|
|
108
|
+
const secure = window.location.protocol === "https:" ? "; Secure" : "";
|
|
109
|
+
document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(value)}; Path=/; Max-Age=${cookieMaxAge}; SameSite=Lax${secure}`;
|
|
110
|
+
if (rawGet("cookie", key) === value && !memory.cookie.has(key)) return true;
|
|
111
|
+
memory.cookie.set(key, value);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const native = nativeStorage(store);
|
|
115
|
+
if (!native) throw new Error("storage unavailable");
|
|
116
|
+
native.setItem(key, value);
|
|
117
|
+
memory[store].delete(key);
|
|
118
|
+
return true;
|
|
119
|
+
} catch {
|
|
120
|
+
memory[store].set(key, value);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
__name(rawSet, "rawSet");
|
|
125
|
+
function rawRemove(store, key) {
|
|
126
|
+
memory[store].delete(key);
|
|
127
|
+
if (!hasWindow()) return;
|
|
128
|
+
try {
|
|
129
|
+
if (store === "cookie") {
|
|
130
|
+
const secure = window.location.protocol === "https:" ? "; Secure" : "";
|
|
131
|
+
document.cookie = `${encodeURIComponent(key)}=; Path=/; Max-Age=0; SameSite=Lax${secure}`;
|
|
132
|
+
} else {
|
|
133
|
+
nativeStorage(store)?.removeItem(key);
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
__name(rawRemove, "rawRemove");
|
|
139
|
+
function resolveStore(key, store) {
|
|
140
|
+
return store ?? specByKey.get(key)?.[0]?.store ?? "local";
|
|
141
|
+
}
|
|
142
|
+
__name(resolveStore, "resolveStore");
|
|
143
|
+
function listKeys(store, startsWith) {
|
|
144
|
+
const out = [];
|
|
145
|
+
try {
|
|
146
|
+
const native = nativeStorage(store);
|
|
147
|
+
if (native) {
|
|
148
|
+
for (let i = 0; i < native.length; i += 1) {
|
|
149
|
+
const name = native.key(i);
|
|
150
|
+
if (name?.startsWith(startsWith)) out.push(name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
for (const name of memory[store].keys()) if (name.startsWith(startsWith) && !out.includes(name)) out.push(name);
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
__name(listKeys, "listKeys");
|
|
159
|
+
const familyKey = /* @__PURE__ */ __name((family, id) => {
|
|
160
|
+
const part = normalizeSegment(id);
|
|
161
|
+
if (!part) throw new TypeError("storage family id is empty after normalisation");
|
|
162
|
+
return family.prefix + part;
|
|
163
|
+
}, "familyKey");
|
|
164
|
+
const api = {
|
|
165
|
+
owner,
|
|
166
|
+
key(...parts) {
|
|
167
|
+
const segments = parts.map(normalizeSegment);
|
|
168
|
+
if (segments.length === 0 || segments.some((segment) => !segment)) {
|
|
169
|
+
throw new TypeError(`storage key parts must be non-empty: ${JSON.stringify(parts)}`);
|
|
170
|
+
}
|
|
171
|
+
return `${prefix}${segments.join(".")}`;
|
|
172
|
+
},
|
|
173
|
+
familyKey,
|
|
174
|
+
isValid,
|
|
175
|
+
read(key, store) {
|
|
176
|
+
assertValid(key);
|
|
177
|
+
const target = resolveStore(key, store);
|
|
178
|
+
return rawGet(target, key);
|
|
179
|
+
},
|
|
180
|
+
write(key, value, store) {
|
|
181
|
+
assertOwned(key);
|
|
182
|
+
const target = resolveStore(key, store);
|
|
183
|
+
return rawSet(target, key, value);
|
|
184
|
+
},
|
|
185
|
+
remove(key, store) {
|
|
186
|
+
assertOwned(key);
|
|
187
|
+
const target = resolveStore(key, store);
|
|
188
|
+
rawRemove(target, key);
|
|
189
|
+
},
|
|
190
|
+
readJSON(key, store) {
|
|
191
|
+
const raw = api.read(key, store);
|
|
192
|
+
if (raw === null) return null;
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(raw);
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
writeJSON(key, value, store) {
|
|
200
|
+
return api.write(key, JSON.stringify(value), store);
|
|
201
|
+
},
|
|
202
|
+
keys(store = "local") {
|
|
203
|
+
return listKeys(store, prefix);
|
|
204
|
+
},
|
|
205
|
+
clearAll({ all = false } = {}) {
|
|
206
|
+
for (const spec of specs) {
|
|
207
|
+
if (!all && !spec.personal) continue;
|
|
208
|
+
rawRemove(spec.store, spec.key);
|
|
209
|
+
}
|
|
210
|
+
for (const family of families) {
|
|
211
|
+
if (!all && !family.personal) continue;
|
|
212
|
+
for (const name of listKeys(family.store, family.prefix)) rawRemove(family.store, name);
|
|
213
|
+
}
|
|
214
|
+
if (all) {
|
|
215
|
+
for (const store of ["local", "session"]) {
|
|
216
|
+
for (const name of listKeys(store, prefix)) rawRemove(store, name);
|
|
217
|
+
}
|
|
218
|
+
for (const name of cookieNames()) if (name.startsWith(prefix)) rawRemove("cookie", name);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
onChange(key, listener, store = "local") {
|
|
222
|
+
assertValid(key);
|
|
223
|
+
if (!hasWindow()) return () => {
|
|
224
|
+
};
|
|
225
|
+
const handler = /* @__PURE__ */ __name((event) => {
|
|
226
|
+
if (event.storageArea !== nativeStorage(store)) return;
|
|
227
|
+
if (event.key === null || event.key === key) listener(event.key === null ? null : event.newValue);
|
|
228
|
+
}, "handler");
|
|
229
|
+
window.addEventListener("storage", handler);
|
|
230
|
+
return () => window.removeEventListener("storage", handler);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
return api;
|
|
234
|
+
}
|
|
235
|
+
__name(createStorage, "createStorage");
|
|
236
|
+
var cfgStorage = createStorage("cfg", {
|
|
237
|
+
keys: CFG_KEYS,
|
|
238
|
+
families: CFG_FAMILIES
|
|
239
|
+
});
|
|
240
|
+
export {
|
|
241
|
+
CFG_FAMILIES,
|
|
242
|
+
CFG_KEYS,
|
|
243
|
+
cfgStorage,
|
|
244
|
+
createStorage,
|
|
245
|
+
normalizeSegment
|
|
246
|
+
};
|
|
247
|
+
//# sourceMappingURL=storage.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/_api/generated/helpers/keys.ts"],"sourcesContent":["// AUTO-GENERATED by django_generator / ts_extras.wrapper\n// Browser-storage key registry: one scheme, guarded access.\n// DO NOT EDIT — re-run `make gen`.\n//\n// Rules for agents and apps: @docs/STORAGE_KEYS.md in @djangocfg/api.\n//\n// Scheme: `<owner>.<area>.<name>` — lower case, dots between levels,\n// `snake_case` inside a level. The framework's owner is `cfg`; an app picks its\n// own with `createStorage('<owner>')`. Third-party keys (a library's own\n// prefix, `NEXT_LOCALE`) are not ours to rename and stay as they are.\n\nexport type KeyStore = 'local' | 'session' | 'cookie';\n\nexport interface KeySpec {\n /** The canonical key. */\n readonly key: string;\n readonly store: KeyStore;\n /** Removed by `clearAll()` (sign-out). Preferences are kept. */\n readonly personal: boolean;\n}\n\nexport interface FamilySpec {\n /** Canonical prefix ending in a dot; the id is appended in normalised form. */\n readonly prefix: string;\n readonly store: 'local' | 'session';\n readonly personal: boolean;\n}\n\nconst k = (\n key: string,\n store: KeyStore,\n personal: boolean,\n): KeySpec => ({ key, store, personal });\n\nconst f = (\n prefix: string,\n store: 'local' | 'session',\n personal: boolean,\n): FamilySpec => ({ prefix, store, personal });\n\n/** Every key the framework writes. No legacy spellings are read: old keys are simply ignored. */\nexport const CFG_KEYS = {\n accessToken: k('cfg.auth.access_token', 'local', true),\n refreshToken: k('cfg.auth.refresh_token', 'local', true),\n apiKey: k('cfg.auth.api_key', 'local', true),\n email: k('cfg.auth.email', 'local', false),\n emailTs: k('cfg.auth.email_ts', 'local', false),\n redirectUrl: k('cfg.auth.redirect_url', 'session', true),\n redirectUrlTs: k('cfg.auth.redirect_url_ts', 'session', true),\n oauthState: k('cfg.auth.oauth_state', 'session', true),\n oauthProvider: k('cfg.auth.oauth_provider', 'session', true),\n profileCache: k('cfg.profile.cache', 'local', true),\n monitorSessionId: k('cfg.monitor.session_id', 'local', false),\n monitorSessionCookie: k('cfg.monitor.session_id', 'cookie', false),\n monitorDebug: k('cfg.monitor.debug', 'local', false),\n uiTabId: k('cfg.ui.tab_id', 'session', false),\n uiRouterSerial: k('cfg.ui.router_serial', 'session', false),\n uiState: k('cfg.ui.state', 'local', false),\n chatSpeechLanguage: k('cfg.chat.speech_language', 'local', false),\n chatRecentTools: k('cfg.chat.recent_tools', 'local', false),\n formsSpeechDebug: k('cfg.forms.speech_debug', 'local', false),\n formsSpeechPrefs: k('cfg.forms.speech_prefs', 'local', false),\n agentPlaygroundApiKeyId: k('cfg.agent.playground_api_key_id', 'session', true),\n agentPlaygroundBearer: k('cfg.agent.playground_bearer', 'session', true),\n agentEndpointDoc: k('cfg.agent.endpoint_doc', 'session', false),\n} as const satisfies Record<string, KeySpec>;\n\n/** Keys with a caller-supplied id part: one per room, tree, endpoint. */\nexport const CFG_FAMILIES = {\n chatDraft: f('cfg.chat.draft.', 'session', true),\n dataTree: f('cfg.data.tree.', 'local', false),\n agentEndpointDraft: f('cfg.agent.endpoint_draft.', 'local', false),\n} as const satisfies Record<string, FamilySpec>;\n\nconst OWNER_RE = /^[a-z][a-z0-9_]*$/;\n\n/** One segment of a key: lower case, `[a-z0-9_]`, anything else folded to `_`. */\nexport function normalizeSegment(raw: string): string {\n return raw\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9_]+/g, '_')\n .replace(/^_+|_+$/g, '');\n}\n\nexport interface StorageOptions {\n /** Named keys of this owner. */\n readonly keys?: Readonly<Record<string, KeySpec>>;\n readonly families?: Readonly<Record<string, FamilySpec>>;\n /** Cookie lifetime for the `cookie` store. Default 30 days. */\n readonly cookieMaxAgeSeconds?: number;\n}\n\nexport interface GuardedStorage {\n readonly owner: string;\n /** Builds `<owner>.<part>.<part>` from free-form parts; throws if a part is empty. */\n key(...parts: string[]): string;\n /** Builds a family member key: the prefix plus the normalised id. */\n familyKey(family: FamilySpec, id: string): string;\n /** `^(cfg|<owner>)(\\.[a-z0-9_]+)+$`. */\n isValid(key: string): boolean;\n read(key: string, store?: KeyStore): string | null;\n /** True when the value reached the browser's storage; false means it is held in memory for this page only. */\n write(key: string, value: string, store?: KeyStore): boolean;\n /** Removes the key. */\n remove(key: string, store?: KeyStore): void;\n readJSON<T>(key: string, store?: KeyStore): T | null;\n writeJSON(key: string, value: unknown, store?: KeyStore): boolean;\n /** Keys this owner holds in `store`. */\n keys(store?: Exclude<KeyStore, 'cookie'>): string[];\n /** Sign-out: removes the `personal` keys. `{ all: true }` removes everything under this owner. */\n clearAll(options?: { all?: boolean }): void;\n /** Cross-tab: calls `listener` with the new value when ANOTHER tab changes `key`. Cookies fire no events. */\n onChange(key: string, listener: (value: string | null) => void, store?: Exclude<KeyStore, 'cookie'>): () => void;\n}\n\nconst hasWindow = () => typeof window !== 'undefined';\n\nfunction nativeStorage(store: 'local' | 'session'): Storage | null {\n if (!hasWindow()) return null;\n try {\n return store === 'session' ? window.sessionStorage : window.localStorage;\n } catch {\n return null;\n }\n}\n\nfunction cookieNames(): string[] {\n if (!hasWindow()) return [];\n try {\n return document.cookie\n .split('; ')\n .map((part) => decodeURIComponent(part.split('=')[0] ?? ''))\n .filter(Boolean);\n } catch {\n return [];\n }\n}\n\nexport function createStorage(owner: string, options: StorageOptions = {}): GuardedStorage {\n if (!OWNER_RE.test(owner)) {\n throw new TypeError(`createStorage: owner \"${owner}\" must match ${OWNER_RE}`);\n }\n const prefix = `${owner}.`;\n const pattern = owner === 'cfg' ? /^cfg(\\.[a-z0-9_]+)+$/ : new RegExp(`^(cfg|${owner})(\\\\.[a-z0-9_]+)+$`);\n const cookieMaxAge = options.cookieMaxAgeSeconds ?? 60 * 60 * 24 * 30;\n const specs = Object.values(options.keys ?? {});\n const families = Object.values(options.families ?? {});\n const specByKey = new Map<string, KeySpec[]>();\n for (const spec of specs) specByKey.set(spec.key, [...(specByKey.get(spec.key) ?? []), spec]);\n // Held in memory when the browser refuses a write (quota, private mode), so the page still works until reload.\n const memory: Record<KeyStore, Map<string, string>> = {\n local: new Map(),\n session: new Map(),\n cookie: new Map(),\n };\n\n const isValid = (key: string) => pattern.test(key);\n const assertValid = (key: string) => {\n if (!isValid(key)) throw new TypeError(`storage key \"${key}\" must match ${pattern}`);\n };\n const assertOwned = (key: string) => {\n assertValid(key);\n if (!key.startsWith(prefix)) throw new TypeError(`storage key \"${key}\" is not owned by \"${owner}\"`);\n };\n\n function rawGet(store: KeyStore, key: string): string | null {\n if (!hasWindow()) return null;\n if (store === 'cookie') {\n try {\n const escaped = encodeURIComponent(key).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const match = document.cookie.match(new RegExp(`(?:^|;\\\\s*)${escaped}=([^;]*)`));\n if (match?.[1] !== undefined) return decodeURIComponent(match[1]);\n } catch {\n /* fall through to memory */\n }\n return memory.cookie.get(key) ?? null;\n }\n try {\n const value = nativeStorage(store)?.getItem(key) ?? null;\n if (value !== null) return value;\n } catch {\n /* fall through to memory */\n }\n return memory[store].get(key) ?? null;\n }\n\n function rawSet(store: KeyStore, key: string, value: string): boolean {\n if (!hasWindow()) return false;\n try {\n if (store === 'cookie') {\n const secure = window.location.protocol === 'https:' ? '; Secure' : '';\n document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(value)}; Path=/; Max-Age=${cookieMaxAge}; SameSite=Lax${secure}`;\n if (rawGet('cookie', key) === value && !memory.cookie.has(key)) return true;\n memory.cookie.set(key, value);\n return false;\n }\n const native = nativeStorage(store);\n if (!native) throw new Error('storage unavailable');\n native.setItem(key, value);\n memory[store].delete(key);\n return true;\n } catch {\n memory[store].set(key, value);\n return false;\n }\n }\n\n function rawRemove(store: KeyStore, key: string): void {\n memory[store].delete(key);\n if (!hasWindow()) return;\n try {\n if (store === 'cookie') {\n const secure = window.location.protocol === 'https:' ? '; Secure' : '';\n document.cookie = `${encodeURIComponent(key)}=; Path=/; Max-Age=0; SameSite=Lax${secure}`;\n } else {\n nativeStorage(store)?.removeItem(key);\n }\n } catch {\n /* ignore */\n }\n }\n\n function resolveStore(key: string, store?: KeyStore): KeyStore {\n return store ?? specByKey.get(key)?.[0]?.store ?? 'local';\n }\n\n function listKeys(store: 'local' | 'session', startsWith: string): string[] {\n const out: string[] = [];\n try {\n const native = nativeStorage(store);\n if (native) {\n for (let i = 0; i < native.length; i += 1) {\n const name = native.key(i);\n if (name?.startsWith(startsWith)) out.push(name);\n }\n }\n } catch {\n /* ignore */\n }\n for (const name of memory[store].keys()) if (name.startsWith(startsWith) && !out.includes(name)) out.push(name);\n return out;\n }\n\n const familyKey = (family: FamilySpec, id: string) => {\n const part = normalizeSegment(id);\n if (!part) throw new TypeError('storage family id is empty after normalisation');\n return family.prefix + part;\n };\n\n const api: GuardedStorage = {\n owner,\n key(...parts) {\n const segments = parts.map(normalizeSegment);\n if (segments.length === 0 || segments.some((segment) => !segment)) {\n throw new TypeError(`storage key parts must be non-empty: ${JSON.stringify(parts)}`);\n }\n return `${prefix}${segments.join('.')}`;\n },\n familyKey,\n isValid,\n read(key, store) {\n assertValid(key);\n const target = resolveStore(key, store);\n return rawGet(target, key);\n },\n write(key, value, store) {\n assertOwned(key);\n const target = resolveStore(key, store);\n return rawSet(target, key, value);\n },\n remove(key, store) {\n assertOwned(key);\n const target = resolveStore(key, store);\n rawRemove(target, key);\n },\n readJSON<T>(key: string, store?: KeyStore): T | null {\n const raw = api.read(key, store);\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n },\n writeJSON(key, value, store) {\n return api.write(key, JSON.stringify(value), store);\n },\n keys(store = 'local') {\n return listKeys(store, prefix);\n },\n clearAll({ all = false } = {}) {\n for (const spec of specs) {\n if (!all && !spec.personal) continue;\n rawRemove(spec.store, spec.key);\n }\n for (const family of families) {\n if (!all && !family.personal) continue;\n for (const name of listKeys(family.store, family.prefix)) rawRemove(family.store, name);\n }\n if (all) {\n for (const store of ['local', 'session'] as const) {\n for (const name of listKeys(store, prefix)) rawRemove(store, name);\n }\n for (const name of cookieNames()) if (name.startsWith(prefix)) rawRemove('cookie', name);\n }\n },\n onChange(key, listener, store = 'local') {\n assertValid(key);\n if (!hasWindow()) return () => {};\n const handler = (event: StorageEvent) => {\n if (event.storageArea !== nativeStorage(store)) return;\n if (event.key === null || event.key === key) listener(event.key === null ? null : event.newValue);\n };\n window.addEventListener('storage', handler);\n return () => window.removeEventListener('storage', handler);\n },\n };\n return api;\n}\n\n/** The framework's own storage: every `cfg.*` key. */\nexport const cfgStorage: GuardedStorage = createStorage('cfg', {\n keys: CFG_KEYS,\n families: CFG_FAMILIES,\n});\n\n"],"mappings":";;;;AA4BA,IAAM,IAAI,wBACR,KACA,OACA,cACa,EAAE,KAAK,OAAO,SAAS,IAJ5B;AAMV,IAAM,IAAI,wBACR,QACA,OACA,cACgB,EAAE,QAAQ,OAAO,SAAS,IAJlC;AAOH,IAAM,WAAW;AAAA,EACtB,aAAa,EAAE,yBAAyB,SAAS,IAAI;AAAA,EACrD,cAAc,EAAE,0BAA0B,SAAS,IAAI;AAAA,EACvD,QAAQ,EAAE,oBAAoB,SAAS,IAAI;AAAA,EAC3C,OAAO,EAAE,kBAAkB,SAAS,KAAK;AAAA,EACzC,SAAS,EAAE,qBAAqB,SAAS,KAAK;AAAA,EAC9C,aAAa,EAAE,yBAAyB,WAAW,IAAI;AAAA,EACvD,eAAe,EAAE,4BAA4B,WAAW,IAAI;AAAA,EAC5D,YAAY,EAAE,wBAAwB,WAAW,IAAI;AAAA,EACrD,eAAe,EAAE,2BAA2B,WAAW,IAAI;AAAA,EAC3D,cAAc,EAAE,qBAAqB,SAAS,IAAI;AAAA,EAClD,kBAAkB,EAAE,0BAA0B,SAAS,KAAK;AAAA,EAC5D,sBAAsB,EAAE,0BAA0B,UAAU,KAAK;AAAA,EACjE,cAAc,EAAE,qBAAqB,SAAS,KAAK;AAAA,EACnD,SAAS,EAAE,iBAAiB,WAAW,KAAK;AAAA,EAC5C,gBAAgB,EAAE,wBAAwB,WAAW,KAAK;AAAA,EAC1D,SAAS,EAAE,gBAAgB,SAAS,KAAK;AAAA,EACzC,oBAAoB,EAAE,4BAA4B,SAAS,KAAK;AAAA,EAChE,iBAAiB,EAAE,yBAAyB,SAAS,KAAK;AAAA,EAC1D,kBAAkB,EAAE,0BAA0B,SAAS,KAAK;AAAA,EAC5D,kBAAkB,EAAE,0BAA0B,SAAS,KAAK;AAAA,EAC5D,yBAAyB,EAAE,mCAAmC,WAAW,IAAI;AAAA,EAC7E,uBAAuB,EAAE,+BAA+B,WAAW,IAAI;AAAA,EACvE,kBAAkB,EAAE,0BAA0B,WAAW,KAAK;AAChE;AAGO,IAAM,eAAe;AAAA,EAC1B,WAAW,EAAE,mBAAmB,WAAW,IAAI;AAAA,EAC/C,UAAU,EAAE,kBAAkB,SAAS,KAAK;AAAA,EAC5C,oBAAoB,EAAE,6BAA6B,SAAS,KAAK;AACnE;AAEA,IAAM,WAAW;AAGV,SAAS,iBAAiB,KAAqB;AACpD,SAAO,IACJ,KAAK,EACL,YAAY,EACZ,QAAQ,gBAAgB,GAAG,EAC3B,QAAQ,YAAY,EAAE;AAC3B;AANgB;AAuChB,IAAM,YAAY,6BAAM,OAAO,WAAW,aAAxB;AAElB,SAAS,cAAc,OAA4C;AACjE,MAAI,CAAC,UAAU,EAAG,QAAO;AACzB,MAAI;AACF,WAAO,UAAU,YAAY,OAAO,iBAAiB,OAAO;AAAA,EAC9D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAPS;AAST,SAAS,cAAwB;AAC/B,MAAI,CAAC,UAAU,EAAG,QAAO,CAAC;AAC1B,MAAI;AACF,WAAO,SAAS,OACb,MAAM,IAAI,EACV,IAAI,CAAC,SAAS,mBAAmB,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,EAC1D,OAAO,OAAO;AAAA,EACnB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAVS;AAYF,SAAS,cAAc,OAAe,UAA0B,CAAC,GAAmB;AACzF,MAAI,CAAC,SAAS,KAAK,KAAK,GAAG;AACzB,UAAM,IAAI,UAAU,yBAAyB,KAAK,gBAAgB,QAAQ,EAAE;AAAA,EAC9E;AACA,QAAM,SAAS,GAAG,KAAK;AACvB,QAAM,UAAU,UAAU,QAAQ,yBAAyB,IAAI,OAAO,SAAS,KAAK,oBAAoB;AACxG,QAAM,eAAe,QAAQ,uBAAuB,KAAK,KAAK,KAAK;AACnE,QAAM,QAAQ,OAAO,OAAO,QAAQ,QAAQ,CAAC,CAAC;AAC9C,QAAM,WAAW,OAAO,OAAO,QAAQ,YAAY,CAAC,CAAC;AACrD,QAAM,YAAY,oBAAI,IAAuB;AAC7C,aAAW,QAAQ,MAAO,WAAU,IAAI,KAAK,KAAK,CAAC,GAAI,UAAU,IAAI,KAAK,GAAG,KAAK,CAAC,GAAI,IAAI,CAAC;AAE5F,QAAM,SAAgD;AAAA,IACpD,OAAO,oBAAI,IAAI;AAAA,IACf,SAAS,oBAAI,IAAI;AAAA,IACjB,QAAQ,oBAAI,IAAI;AAAA,EAClB;AAEA,QAAM,UAAU,wBAAC,QAAgB,QAAQ,KAAK,GAAG,GAAjC;AAChB,QAAM,cAAc,wBAAC,QAAgB;AACnC,QAAI,CAAC,QAAQ,GAAG,EAAG,OAAM,IAAI,UAAU,gBAAgB,GAAG,gBAAgB,OAAO,EAAE;AAAA,EACrF,GAFoB;AAGpB,QAAM,cAAc,wBAAC,QAAgB;AACnC,gBAAY,GAAG;AACf,QAAI,CAAC,IAAI,WAAW,MAAM,EAAG,OAAM,IAAI,UAAU,gBAAgB,GAAG,sBAAsB,KAAK,GAAG;AAAA,EACpG,GAHoB;AAKpB,WAAS,OAAO,OAAiB,KAA4B;AAC3D,QAAI,CAAC,UAAU,EAAG,QAAO;AACzB,QAAI,UAAU,UAAU;AACtB,UAAI;AACF,cAAM,UAAU,mBAAmB,GAAG,EAAE,QAAQ,uBAAuB,MAAM;AAC7E,cAAM,QAAQ,SAAS,OAAO,MAAM,IAAI,OAAO,cAAc,OAAO,UAAU,CAAC;AAC/E,YAAI,QAAQ,CAAC,MAAM,OAAW,QAAO,mBAAmB,MAAM,CAAC,CAAC;AAAA,MAClE,QAAQ;AAAA,MAER;AACA,aAAO,OAAO,OAAO,IAAI,GAAG,KAAK;AAAA,IACnC;AACA,QAAI;AACF,YAAM,QAAQ,cAAc,KAAK,GAAG,QAAQ,GAAG,KAAK;AACpD,UAAI,UAAU,KAAM,QAAO;AAAA,IAC7B,QAAQ;AAAA,IAER;AACA,WAAO,OAAO,KAAK,EAAE,IAAI,GAAG,KAAK;AAAA,EACnC;AAnBS;AAqBT,WAAS,OAAO,OAAiB,KAAa,OAAwB;AACpE,QAAI,CAAC,UAAU,EAAG,QAAO;AACzB,QAAI;AACF,UAAI,UAAU,UAAU;AACtB,cAAM,SAAS,OAAO,SAAS,aAAa,WAAW,aAAa;AACpE,iBAAS,SAAS,GAAG,mBAAmB,GAAG,CAAC,IAAI,mBAAmB,KAAK,CAAC,qBAAqB,YAAY,iBAAiB,MAAM;AACjI,YAAI,OAAO,UAAU,GAAG,MAAM,SAAS,CAAC,OAAO,OAAO,IAAI,GAAG,EAAG,QAAO;AACvE,eAAO,OAAO,IAAI,KAAK,KAAK;AAC5B,eAAO;AAAA,MACT;AACA,YAAM,SAAS,cAAc,KAAK;AAClC,UAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,qBAAqB;AAClD,aAAO,QAAQ,KAAK,KAAK;AACzB,aAAO,KAAK,EAAE,OAAO,GAAG;AACxB,aAAO;AAAA,IACT,QAAQ;AACN,aAAO,KAAK,EAAE,IAAI,KAAK,KAAK;AAC5B,aAAO;AAAA,IACT;AAAA,EACF;AAnBS;AAqBT,WAAS,UAAU,OAAiB,KAAmB;AACrD,WAAO,KAAK,EAAE,OAAO,GAAG;AACxB,QAAI,CAAC,UAAU,EAAG;AAClB,QAAI;AACF,UAAI,UAAU,UAAU;AACtB,cAAM,SAAS,OAAO,SAAS,aAAa,WAAW,aAAa;AACpE,iBAAS,SAAS,GAAG,mBAAmB,GAAG,CAAC,qCAAqC,MAAM;AAAA,MACzF,OAAO;AACL,sBAAc,KAAK,GAAG,WAAW,GAAG;AAAA,MACtC;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAbS;AAeT,WAAS,aAAa,KAAa,OAA4B;AAC7D,WAAO,SAAS,UAAU,IAAI,GAAG,IAAI,CAAC,GAAG,SAAS;AAAA,EACpD;AAFS;AAIT,WAAS,SAAS,OAA4B,YAA8B;AAC1E,UAAM,MAAgB,CAAC;AACvB,QAAI;AACF,YAAM,SAAS,cAAc,KAAK;AAClC,UAAI,QAAQ;AACV,iBAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,GAAG;AACzC,gBAAM,OAAO,OAAO,IAAI,CAAC;AACzB,cAAI,MAAM,WAAW,UAAU,EAAG,KAAI,KAAK,IAAI;AAAA,QACjD;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AACA,eAAW,QAAQ,OAAO,KAAK,EAAE,KAAK,EAAG,KAAI,KAAK,WAAW,UAAU,KAAK,CAAC,IAAI,SAAS,IAAI,EAAG,KAAI,KAAK,IAAI;AAC9G,WAAO;AAAA,EACT;AAfS;AAiBT,QAAM,YAAY,wBAAC,QAAoB,OAAe;AACpD,UAAM,OAAO,iBAAiB,EAAE;AAChC,QAAI,CAAC,KAAM,OAAM,IAAI,UAAU,gDAAgD;AAC/E,WAAO,OAAO,SAAS;AAAA,EACzB,GAJkB;AAMlB,QAAM,MAAsB;AAAA,IAC1B;AAAA,IACA,OAAO,OAAO;AACZ,YAAM,WAAW,MAAM,IAAI,gBAAgB;AAC3C,UAAI,SAAS,WAAW,KAAK,SAAS,KAAK,CAAC,YAAY,CAAC,OAAO,GAAG;AACjE,cAAM,IAAI,UAAU,wCAAwC,KAAK,UAAU,KAAK,CAAC,EAAE;AAAA,MACrF;AACA,aAAO,GAAG,MAAM,GAAG,SAAS,KAAK,GAAG,CAAC;AAAA,IACvC;AAAA,IACA;AAAA,IACA;AAAA,IACA,KAAK,KAAK,OAAO;AACf,kBAAY,GAAG;AACf,YAAM,SAAS,aAAa,KAAK,KAAK;AACtC,aAAO,OAAO,QAAQ,GAAG;AAAA,IAC3B;AAAA,IACA,MAAM,KAAK,OAAO,OAAO;AACvB,kBAAY,GAAG;AACf,YAAM,SAAS,aAAa,KAAK,KAAK;AACtC,aAAO,OAAO,QAAQ,KAAK,KAAK;AAAA,IAClC;AAAA,IACA,OAAO,KAAK,OAAO;AACjB,kBAAY,GAAG;AACf,YAAM,SAAS,aAAa,KAAK,KAAK;AACtC,gBAAU,QAAQ,GAAG;AAAA,IACvB;AAAA,IACA,SAAY,KAAa,OAA4B;AACnD,YAAM,MAAM,IAAI,KAAK,KAAK,KAAK;AAC/B,UAAI,QAAQ,KAAM,QAAO;AACzB,UAAI;AACF,eAAO,KAAK,MAAM,GAAG;AAAA,MACvB,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,UAAU,KAAK,OAAO,OAAO;AAC3B,aAAO,IAAI,MAAM,KAAK,KAAK,UAAU,KAAK,GAAG,KAAK;AAAA,IACpD;AAAA,IACA,KAAK,QAAQ,SAAS;AACpB,aAAO,SAAS,OAAO,MAAM;AAAA,IAC/B;AAAA,IACA,SAAS,EAAE,MAAM,MAAM,IAAI,CAAC,GAAG;AAC7B,iBAAW,QAAQ,OAAO;AACxB,YAAI,CAAC,OAAO,CAAC,KAAK,SAAU;AAC5B,kBAAU,KAAK,OAAO,KAAK,GAAG;AAAA,MAChC;AACA,iBAAW,UAAU,UAAU;AAC7B,YAAI,CAAC,OAAO,CAAC,OAAO,SAAU;AAC9B,mBAAW,QAAQ,SAAS,OAAO,OAAO,OAAO,MAAM,EAAG,WAAU,OAAO,OAAO,IAAI;AAAA,MACxF;AACA,UAAI,KAAK;AACP,mBAAW,SAAS,CAAC,SAAS,SAAS,GAAY;AACjD,qBAAW,QAAQ,SAAS,OAAO,MAAM,EAAG,WAAU,OAAO,IAAI;AAAA,QACnE;AACA,mBAAW,QAAQ,YAAY,EAAG,KAAI,KAAK,WAAW,MAAM,EAAG,WAAU,UAAU,IAAI;AAAA,MACzF;AAAA,IACF;AAAA,IACA,SAAS,KAAK,UAAU,QAAQ,SAAS;AACvC,kBAAY,GAAG;AACf,UAAI,CAAC,UAAU,EAAG,QAAO,MAAM;AAAA,MAAC;AAChC,YAAM,UAAU,wBAAC,UAAwB;AACvC,YAAI,MAAM,gBAAgB,cAAc,KAAK,EAAG;AAChD,YAAI,MAAM,QAAQ,QAAQ,MAAM,QAAQ,IAAK,UAAS,MAAM,QAAQ,OAAO,OAAO,MAAM,QAAQ;AAAA,MAClG,GAHgB;AAIhB,aAAO,iBAAiB,WAAW,OAAO;AAC1C,aAAO,MAAM,OAAO,oBAAoB,WAAW,OAAO;AAAA,IAC5D;AAAA,EACF;AACA,SAAO;AACT;AApLgB;AAuLT,IAAM,aAA6B,cAAc,OAAO;AAAA,EAC7D,MAAM;AAAA,EACN,UAAU;AACZ,CAAC;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/api",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.584",
|
|
4
4
|
"description": "Auto-generated TypeScript API client with React hooks, SWR integration, and Zod validation for Django REST Framework backends",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"django",
|
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
"import": "./dist/auth-server.mjs",
|
|
51
51
|
"require": "./dist/auth-server.cjs"
|
|
52
52
|
},
|
|
53
|
+
"./storage": {
|
|
54
|
+
"types": "./dist/storage.d.ts",
|
|
55
|
+
"import": "./dist/storage.mjs",
|
|
56
|
+
"require": "./dist/storage.cjs"
|
|
57
|
+
},
|
|
53
58
|
"./clients": {
|
|
54
59
|
"types": "./dist/clients.d.ts",
|
|
55
60
|
"import": "./dist/clients.mjs",
|
|
@@ -64,6 +69,7 @@
|
|
|
64
69
|
"files": [
|
|
65
70
|
"dist",
|
|
66
71
|
"src",
|
|
72
|
+
"@docs",
|
|
67
73
|
"README.md",
|
|
68
74
|
"LICENSE"
|
|
69
75
|
],
|
|
@@ -85,7 +91,7 @@
|
|
|
85
91
|
"zod": "^4.3.6"
|
|
86
92
|
},
|
|
87
93
|
"devDependencies": {
|
|
88
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
94
|
+
"@djangocfg/typescript-config": "^2.1.584",
|
|
89
95
|
"@testing-library/react": "^16.3.2",
|
|
90
96
|
"@types/node": "^25.9.5",
|
|
91
97
|
"@types/react": "19.2.15",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
// Storage keys are defined once, in the registry. A literal anywhere else is a bug.
|
|
6
|
+
const FORBIDDEN = [
|
|
7
|
+
'user_profile_cache', 'auth_email', 'auth_redirect_url', 'fm_session_id', 'oauth_state',
|
|
8
|
+
'oauth_provider', 'cfg.access_token', 'cfg.refresh_token', 'cfg.api_key',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function walk(dir: string, out: string[] = []): string[] {
|
|
12
|
+
for (const name of readdirSync(dir)) {
|
|
13
|
+
const path = join(dir, name);
|
|
14
|
+
if (statSync(path).isDirectory()) {
|
|
15
|
+
if (name !== '__tests__' && name !== 'generated') walk(path, out);
|
|
16
|
+
} else if (/\.tsx?$/.test(name)) out.push(path);
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('no key literals outside the registry', () => {
|
|
22
|
+
const files = walk(join(__dirname, '..'));
|
|
23
|
+
it.each(FORBIDDEN)('"%s" is not spelled in source', (literal) => {
|
|
24
|
+
const hits = files.filter((f) => readFileSync(f, 'utf8').includes(`'${literal}'`) || readFileSync(f, 'utf8').includes(`"${literal}"`));
|
|
25
|
+
expect(hits).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { CFG_FAMILIES, CFG_KEYS, cfgStorage, createStorage, normalizeSegment } from '../storage';
|
|
5
|
+
|
|
6
|
+
const SCHEME = /^cfg(\.[a-z0-9_]+)+$/;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
localStorage.clear();
|
|
10
|
+
sessionStorage.clear();
|
|
11
|
+
document.cookie.split('; ').forEach((c) => {
|
|
12
|
+
document.cookie = `${c.split('=')[0]}=; Max-Age=0; Path=/`;
|
|
13
|
+
});
|
|
14
|
+
cfgStorage.clearAll({ all: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('registry', () => {
|
|
18
|
+
it.each(Object.entries(CFG_KEYS))('%s follows the scheme and round-trips', (_name, spec) => {
|
|
19
|
+
expect(spec.key).toMatch(SCHEME);
|
|
20
|
+
expect(cfgStorage.write(spec.key, 'v', spec.store)).toBe(true);
|
|
21
|
+
expect(cfgStorage.read(spec.key, spec.store)).toBe('v');
|
|
22
|
+
cfgStorage.remove(spec.key, spec.store);
|
|
23
|
+
expect(cfgStorage.read(spec.key, spec.store)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('has no duplicate key within one store', () => {
|
|
27
|
+
const seen = new Set<string>();
|
|
28
|
+
for (const spec of Object.values(CFG_KEYS)) {
|
|
29
|
+
const id = `${spec.store}:${spec.key}`;
|
|
30
|
+
expect(seen.has(id)).toBe(false);
|
|
31
|
+
seen.add(id);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.each(Object.entries(CFG_FAMILIES))('family %s prefix follows the scheme', (_name, family) => {
|
|
36
|
+
expect(family.prefix).toMatch(/^cfg(\.[a-z0-9_]+)+\.$/);
|
|
37
|
+
expect(cfgStorage.familyKey(family, 'Room 1/A')).toBe(`${family.prefix}room_1_a`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('does not read old spellings', () => {
|
|
41
|
+
localStorage.setItem('cfg.access_token', 'old');
|
|
42
|
+
expect(cfgStorage.read(CFG_KEYS.accessToken.key)).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('validation', () => {
|
|
47
|
+
it('rejects keys outside the scheme', () => {
|
|
48
|
+
for (const bad of ['auth_email', 'cfg', 'cfg.', 'cfg.Auth.x', 'cfg.auth-x', 'other.x']) {
|
|
49
|
+
expect(() => cfgStorage.read(bad)).toThrow(TypeError);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('app storage reads cfg and own keys, writes only its own', () => {
|
|
54
|
+
const app = createStorage('kino');
|
|
55
|
+
expect(app.isValid('kino.player.volume')).toBe(true);
|
|
56
|
+
expect(app.isValid('cfg.auth.email')).toBe(true);
|
|
57
|
+
expect(app.isValid('other.x')).toBe(false);
|
|
58
|
+
expect(() => app.write('cfg.auth.email', 'x')).toThrow(TypeError);
|
|
59
|
+
expect(app.write('kino.player.volume', '3')).toBe(true);
|
|
60
|
+
expect(app.read('kino.player.volume')).toBe('3');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('builds and normalises keys from parts', () => {
|
|
64
|
+
const app = createStorage('kino');
|
|
65
|
+
expect(app.key('Player', 'Last Volume')).toBe('kino.player.last_volume');
|
|
66
|
+
expect(() => app.key('player', ' ')).toThrow(TypeError);
|
|
67
|
+
expect(() => createStorage('Bad-Owner')).toThrow(TypeError);
|
|
68
|
+
expect(normalizeSegment(' A-b c ')).toBe('a_b_c');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('guarded I/O', () => {
|
|
73
|
+
it('keeps the value in memory when the browser refuses a write', () => {
|
|
74
|
+
const spy = vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
75
|
+
throw new DOMException('full', 'QuotaExceededError');
|
|
76
|
+
});
|
|
77
|
+
expect(cfgStorage.write(CFG_KEYS.email.key, 'a@b.c')).toBe(false);
|
|
78
|
+
expect(cfgStorage.read(CFG_KEYS.email.key)).toBe('a@b.c');
|
|
79
|
+
spy.mockRestore();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('survives a throwing storage getter', () => {
|
|
83
|
+
const spy = vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
|
84
|
+
throw new Error('denied');
|
|
85
|
+
});
|
|
86
|
+
expect(cfgStorage.read(CFG_KEYS.email.key)).toBeNull();
|
|
87
|
+
spy.mockRestore();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('readJSON returns null on garbage', () => {
|
|
91
|
+
localStorage.setItem(CFG_KEYS.uiState.key, '{oops');
|
|
92
|
+
expect(cfgStorage.readJSON(CFG_KEYS.uiState.key)).toBeNull();
|
|
93
|
+
cfgStorage.writeJSON(CFG_KEYS.uiState.key, { a: 1 });
|
|
94
|
+
expect(cfgStorage.readJSON(CFG_KEYS.uiState.key)).toEqual({ a: 1 });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('clearAll', () => {
|
|
99
|
+
it('removes personal keys and family members, keeps preferences', () => {
|
|
100
|
+
cfgStorage.write(CFG_KEYS.accessToken.key, 't');
|
|
101
|
+
cfgStorage.write(CFG_KEYS.chatSpeechLanguage.key, 'ru');
|
|
102
|
+
cfgStorage.write(cfgStorage.familyKey(CFG_FAMILIES.chatDraft, 'r1'), 'draft', 'session');
|
|
103
|
+
cfgStorage.write(cfgStorage.familyKey(CFG_FAMILIES.dataTree, 't1'), 'tree');
|
|
104
|
+
cfgStorage.clearAll();
|
|
105
|
+
expect(cfgStorage.read(CFG_KEYS.accessToken.key)).toBeNull();
|
|
106
|
+
expect(cfgStorage.read(cfgStorage.familyKey(CFG_FAMILIES.chatDraft, 'r1'), 'session')).toBeNull();
|
|
107
|
+
expect(cfgStorage.read(CFG_KEYS.chatSpeechLanguage.key)).toBe('ru');
|
|
108
|
+
expect(cfgStorage.read(cfgStorage.familyKey(CFG_FAMILIES.dataTree, 't1'))).toBe('tree');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('{ all: true } wipes the owner namespace but not foreign keys', () => {
|
|
112
|
+
localStorage.setItem('NEXT_LOCALE', 'en');
|
|
113
|
+
cfgStorage.write(CFG_KEYS.chatSpeechLanguage.key, 'ru');
|
|
114
|
+
cfgStorage.write('cfg.custom.thing', '1');
|
|
115
|
+
cfgStorage.clearAll({ all: true });
|
|
116
|
+
expect(localStorage.getItem('cfg.custom.thing')).toBeNull();
|
|
117
|
+
expect(localStorage.getItem('NEXT_LOCALE')).toBe('en');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('cross-tab', () => {
|
|
122
|
+
it('onChange fires for the key from another tab only', () => {
|
|
123
|
+
const listener = vi.fn();
|
|
124
|
+
const off = cfgStorage.onChange(CFG_KEYS.accessToken.key, listener);
|
|
125
|
+
window.dispatchEvent(new StorageEvent('storage', { key: 'cfg.other.x', newValue: '1', storageArea: localStorage }));
|
|
126
|
+
window.dispatchEvent(new StorageEvent('storage', { key: CFG_KEYS.accessToken.key, newValue: 'n', storageArea: sessionStorage }));
|
|
127
|
+
expect(listener).not.toHaveBeenCalled();
|
|
128
|
+
window.dispatchEvent(new StorageEvent('storage', { key: CFG_KEYS.accessToken.key, newValue: 'n', storageArea: localStorage }));
|
|
129
|
+
expect(listener).toHaveBeenCalledWith('n');
|
|
130
|
+
off();
|
|
131
|
+
window.dispatchEvent(new StorageEvent('storage', { key: null, storageArea: localStorage }));
|
|
132
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { CFG_KEYS, cfgStorage } from '../storage';
|
|
4
|
+
|
|
5
|
+
describe('storage on the server', () => {
|
|
6
|
+
it('never throws and never holds state', () => {
|
|
7
|
+
expect(typeof window).toBe('undefined');
|
|
8
|
+
expect(cfgStorage.read(CFG_KEYS.accessToken.key)).toBeNull();
|
|
9
|
+
expect(cfgStorage.write(CFG_KEYS.accessToken.key, 'x')).toBe(false);
|
|
10
|
+
expect(cfgStorage.read(CFG_KEYS.accessToken.key)).toBeNull();
|
|
11
|
+
expect(() => cfgStorage.clearAll({ all: true })).not.toThrow();
|
|
12
|
+
expect(cfgStorage.onChange(CFG_KEYS.accessToken.key, () => {})).toBeTypeOf('function');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -1463,11 +1463,6 @@
|
|
|
1463
1463
|
"type": "string",
|
|
1464
1464
|
"format": "uri",
|
|
1465
1465
|
"description": "Same redirect_uri used in authorize request. If not provided, uses config's site_url + callback_path"
|
|
1466
|
-
},
|
|
1467
|
-
"remember_me": {
|
|
1468
|
-
"type": "boolean",
|
|
1469
|
-
"default": false,
|
|
1470
|
-
"description": "Keep this browser signed in for 30 days"
|
|
1471
1466
|
}
|
|
1472
1467
|
},
|
|
1473
1468
|
"required": [
|
|
@@ -1647,10 +1642,6 @@
|
|
|
1647
1642
|
"should_prompt_2fa": {
|
|
1648
1643
|
"type": "boolean",
|
|
1649
1644
|
"description": "True if user should be prompted to enable 2FA"
|
|
1650
|
-
},
|
|
1651
|
-
"persistent_session": {
|
|
1652
|
-
"type": "boolean",
|
|
1653
|
-
"description": "Whether this login should persist across browser restarts"
|
|
1654
1645
|
}
|
|
1655
1646
|
}
|
|
1656
1647
|
},
|
|
@@ -1764,11 +1755,6 @@
|
|
|
1764
1755
|
"type": "string",
|
|
1765
1756
|
"format": "uri",
|
|
1766
1757
|
"description": "Source URL for tracking login (e.g., https://my.djangocfg.com)"
|
|
1767
|
-
},
|
|
1768
|
-
"remember_me": {
|
|
1769
|
-
"type": "boolean",
|
|
1770
|
-
"default": false,
|
|
1771
|
-
"description": "Keep this browser signed in for 30 days"
|
|
1772
1758
|
}
|
|
1773
1759
|
},
|
|
1774
1760
|
"required": [
|
|
@@ -1821,10 +1807,6 @@
|
|
|
1821
1807
|
"should_prompt_2fa": {
|
|
1822
1808
|
"type": "boolean",
|
|
1823
1809
|
"description": "Whether user should be prompted to enable 2FA"
|
|
1824
|
-
},
|
|
1825
|
-
"persistent_session": {
|
|
1826
|
-
"type": "boolean",
|
|
1827
|
-
"description": "Whether this login should persist across browser restarts"
|
|
1828
1810
|
}
|
|
1829
1811
|
}
|
|
1830
1812
|
},
|
|
@@ -1855,13 +1837,8 @@
|
|
|
1855
1837
|
},
|
|
1856
1838
|
"PasswordLoginRequest": {
|
|
1857
1839
|
"type": "object",
|
|
1858
|
-
"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
|
|
1840
|
+
"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.",
|
|
1859
1841
|
"properties": {
|
|
1860
|
-
"remember_me": {
|
|
1861
|
-
"type": "boolean",
|
|
1862
|
-
"default": false,
|
|
1863
|
-
"description": "Keep this browser signed in for 30 days"
|
|
1864
|
-
},
|
|
1865
1842
|
"email": {
|
|
1866
1843
|
"type": "string",
|
|
1867
1844
|
"format": "email",
|
|
@@ -1923,10 +1900,6 @@
|
|
|
1923
1900
|
"should_prompt_2fa": {
|
|
1924
1901
|
"type": "boolean",
|
|
1925
1902
|
"description": "Whether user should be prompted to enable 2FA"
|
|
1926
|
-
},
|
|
1927
|
-
"persistent_session": {
|
|
1928
|
-
"type": "boolean",
|
|
1929
|
-
"description": "Whether this login should persist across browser restarts"
|
|
1930
1903
|
}
|
|
1931
1904
|
}
|
|
1932
1905
|
},
|
|
@@ -8,7 +8,6 @@ export const OAuthCallbackRequestRequestSchema = z.object({
|
|
|
8
8
|
code: z.string().min(10).max(500),
|
|
9
9
|
state: z.string().min(20).max(100),
|
|
10
10
|
redirect_uri: z.url().or(z.literal("")).optional(),
|
|
11
|
-
remember_me: z.boolean().default(false).optional(),
|
|
12
11
|
});
|
|
13
12
|
|
|
14
13
|
export type OAuthCallbackRequestRequest = z.infer<typeof OAuthCallbackRequestRequestSchema>;
|
|
@@ -13,7 +13,6 @@ export const OAuthTokenResponseSchema = z.object({
|
|
|
13
13
|
is_new_user: z.boolean().default(false).optional(),
|
|
14
14
|
is_new_connection: z.boolean().default(false).optional(),
|
|
15
15
|
should_prompt_2fa: z.boolean().optional(),
|
|
16
|
-
persistent_session: z.boolean().optional(),
|
|
17
16
|
});
|
|
18
17
|
|
|
19
18
|
export type OAuthTokenResponse = z.infer<typeof OAuthTokenResponseSchema>;
|
|
@@ -8,7 +8,6 @@ export const OTPVerifyRequestSchema = z.object({
|
|
|
8
8
|
identifier: z.string().min(1),
|
|
9
9
|
otp: z.string().min(4).max(4),
|
|
10
10
|
source_url: z.url().or(z.literal("")).optional(),
|
|
11
|
-
remember_me: z.boolean().default(false).optional(),
|
|
12
11
|
});
|
|
13
12
|
|
|
14
13
|
export type OTPVerifyRequest = z.infer<typeof OTPVerifyRequestSchema>;
|
|
@@ -12,7 +12,6 @@ export const OTPVerifyResponseSchema = z.object({
|
|
|
12
12
|
access: z.string().nullable().optional(),
|
|
13
13
|
user: UserSchema.nullable().optional(),
|
|
14
14
|
should_prompt_2fa: z.boolean().optional(),
|
|
15
|
-
persistent_session: z.boolean().optional(),
|
|
16
15
|
});
|
|
17
16
|
|
|
18
17
|
export type OTPVerifyResponse = z.infer<typeof OTPVerifyResponseSchema>;
|
|
@@ -12,7 +12,6 @@ export const PasswordLoginResponseSchema = z.object({
|
|
|
12
12
|
access: z.string().nullable().optional(),
|
|
13
13
|
user: UserSchema.nullable().optional(),
|
|
14
14
|
should_prompt_2fa: z.boolean().optional(),
|
|
15
|
-
persistent_session: z.boolean().optional(),
|
|
16
15
|
});
|
|
17
16
|
|
|
18
17
|
export type PasswordLoginResponse = z.infer<typeof PasswordLoginResponseSchema>;
|
|
@@ -902,10 +902,6 @@
|
|
|
902
902
|
],
|
|
903
903
|
"description": "User profile data"
|
|
904
904
|
},
|
|
905
|
-
"persistent_session": {
|
|
906
|
-
"type": "boolean",
|
|
907
|
-
"description": "Whether this login should persist across browser restarts"
|
|
908
|
-
},
|
|
909
905
|
"remaining_backup_codes": {
|
|
910
906
|
"type": "integer",
|
|
911
907
|
"description": "Number of remaining backup codes (if backup code was used)"
|
|
@@ -918,7 +914,6 @@
|
|
|
918
914
|
"required": [
|
|
919
915
|
"access_token",
|
|
920
916
|
"message",
|
|
921
|
-
"persistent_session",
|
|
922
917
|
"refresh_token",
|
|
923
918
|
"user"
|
|
924
919
|
]
|
|
@@ -10,7 +10,6 @@ export const VerifyResponseSchema = z.object({
|
|
|
10
10
|
access_token: z.string(),
|
|
11
11
|
refresh_token: z.string(),
|
|
12
12
|
user: TotpVerifyUserSchema,
|
|
13
|
-
persistent_session: z.boolean(),
|
|
14
13
|
remaining_backup_codes: z.number().int().optional(),
|
|
15
14
|
warning: z.string().optional(),
|
|
16
15
|
});
|