@zucker-framework/fe-storage 1.0.0 → 1.0.5

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 ADDED
@@ -0,0 +1,7 @@
1
+ # Frontend storage
2
+
3
+ `LocalStore` provides the shared browser storage and authentication-key contract, including best-effort handling of unavailable storage.
4
+
5
+ `setCookie(name, value, options?)`, `getCookie(name)` and `removeCookie(name, options?)` manage browser-readable cookies with SSR-safe no-op writes/null reads. Values are percent-encoded; malformed legacy encoding is returned unchanged. Names and attributes are validated before writing. `CookieOptions` supports `path`, `domain`, `expires`, `maxAge` (seconds), `sameSite` and `secure`; no authentication key, lifetime, domain or SameSite policy is hard-coded. Deletion must supply the same path/domain. HttpOnly cookies must be issued by the server, never by this API. New exports require a released artifact before production consumers switch.
6
+
7
+ `downloadBlob(blob, fileName, { revokeDelayMs? }?)` triggers a caller-requested browser download. It owns the temporary anchor and object URL, cleaning both after the click, including click failures. URL cleanup is immediate by default; a caller with an existing delayed-revocation requirement can provide that delay. Applications retain the filename, MIME type and when to download. This helper does not perform HTTP requests and throws outside a browser.
package/dist/index.d.mts CHANGED
@@ -12,6 +12,7 @@ declare const REFRESH_TOKEN_KEY = "REFRESH_TOKEN";
12
12
  * All methods are static so the class can be used without instantiation.
13
13
  */
14
14
  declare class LocalStore {
15
+ private static attempt;
15
16
  /** Store a value under the given key. Non-string values are JSON-serialized. */
16
17
  static set(key: string, data: unknown): void;
17
18
  /** Retrieve and JSON-parse a value by key. Returns null if the key does not exist. */
@@ -35,9 +36,30 @@ declare class LocalStore {
35
36
  /** Retrieve the stored user profile object. */
36
37
  static getUserInfo<T = Record<string, unknown>>(): T | null;
37
38
  /** Persist a user profile object. */
38
- static setUserInfo(userInfo: Record<string, unknown>): void;
39
+ static setUserInfo<TUser extends object>(userInfo: TUser): void;
39
40
  /** Remove all auth-related keys (token, refresh token, user info). */
40
41
  static clearAuth(): void;
41
42
  }
42
43
 
43
- export { LocalStore, REFRESH_TOKEN_KEY, TOKEN_KEY, USER_KEY };
44
+ /** Trigger an explicitly requested browser download and release its temporary DOM/URL resources. */
45
+ declare function downloadBlob(blob: Blob, fileName: string, options?: {
46
+ revokeDelayMs?: number;
47
+ }): void;
48
+
49
+ interface CookieOptions {
50
+ path?: string;
51
+ domain?: string;
52
+ expires?: Date;
53
+ /** Lifetime in seconds, not milliseconds. */
54
+ maxAge?: number;
55
+ sameSite?: 'Strict' | 'Lax' | 'None';
56
+ secure?: boolean;
57
+ }
58
+ /** Browser-readable cookies only. HttpOnly credentials must be set by a server. */
59
+ declare function setCookie(name: string, value: string, options?: CookieOptions): void;
60
+ /** Exact-name lookup; malformed percent encoding is retained as a legacy raw value. */
61
+ declare function getCookie(name: string): string | null;
62
+ /** Deletion must use the same path/domain as the original cookie. */
63
+ declare function removeCookie(name: string, options?: Omit<CookieOptions, 'expires' | 'maxAge'>): void;
64
+
65
+ export { type CookieOptions, LocalStore, REFRESH_TOKEN_KEY, TOKEN_KEY, USER_KEY, downloadBlob, getCookie, removeCookie, setCookie };
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ declare const REFRESH_TOKEN_KEY = "REFRESH_TOKEN";
12
12
  * All methods are static so the class can be used without instantiation.
13
13
  */
14
14
  declare class LocalStore {
15
+ private static attempt;
15
16
  /** Store a value under the given key. Non-string values are JSON-serialized. */
16
17
  static set(key: string, data: unknown): void;
17
18
  /** Retrieve and JSON-parse a value by key. Returns null if the key does not exist. */
@@ -35,9 +36,30 @@ declare class LocalStore {
35
36
  /** Retrieve the stored user profile object. */
36
37
  static getUserInfo<T = Record<string, unknown>>(): T | null;
37
38
  /** Persist a user profile object. */
38
- static setUserInfo(userInfo: Record<string, unknown>): void;
39
+ static setUserInfo<TUser extends object>(userInfo: TUser): void;
39
40
  /** Remove all auth-related keys (token, refresh token, user info). */
40
41
  static clearAuth(): void;
41
42
  }
42
43
 
43
- export { LocalStore, REFRESH_TOKEN_KEY, TOKEN_KEY, USER_KEY };
44
+ /** Trigger an explicitly requested browser download and release its temporary DOM/URL resources. */
45
+ declare function downloadBlob(blob: Blob, fileName: string, options?: {
46
+ revokeDelayMs?: number;
47
+ }): void;
48
+
49
+ interface CookieOptions {
50
+ path?: string;
51
+ domain?: string;
52
+ expires?: Date;
53
+ /** Lifetime in seconds, not milliseconds. */
54
+ maxAge?: number;
55
+ sameSite?: 'Strict' | 'Lax' | 'None';
56
+ secure?: boolean;
57
+ }
58
+ /** Browser-readable cookies only. HttpOnly credentials must be set by a server. */
59
+ declare function setCookie(name: string, value: string, options?: CookieOptions): void;
60
+ /** Exact-name lookup; malformed percent encoding is retained as a legacy raw value. */
61
+ declare function getCookie(name: string): string | null;
62
+ /** Deletion must use the same path/domain as the original cookie. */
63
+ declare function removeCookie(name: string, options?: Omit<CookieOptions, 'expires' | 'maxAge'>): void;
64
+
65
+ export { type CookieOptions, LocalStore, REFRESH_TOKEN_KEY, TOKEN_KEY, USER_KEY, downloadBlob, getCookie, removeCookie, setCookie };
package/dist/index.js CHANGED
@@ -23,7 +23,11 @@ __export(index_exports, {
23
23
  LocalStore: () => LocalStore,
24
24
  REFRESH_TOKEN_KEY: () => REFRESH_TOKEN_KEY,
25
25
  TOKEN_KEY: () => TOKEN_KEY,
26
- USER_KEY: () => USER_KEY
26
+ USER_KEY: () => USER_KEY,
27
+ downloadBlob: () => downloadBlob,
28
+ getCookie: () => getCookie,
29
+ removeCookie: () => removeCookie,
30
+ setCookie: () => setCookie
27
31
  });
28
32
  module.exports = __toCommonJS(index_exports);
29
33
 
@@ -34,14 +38,23 @@ var REFRESH_TOKEN_KEY = "REFRESH_TOKEN";
34
38
 
35
39
  // src/local-store.ts
36
40
  var LocalStore = class {
41
+ static attempt(operation, fallback) {
42
+ try {
43
+ return operation();
44
+ } catch {
45
+ return fallback;
46
+ }
47
+ }
37
48
  /** Store a value under the given key. Non-string values are JSON-serialized. */
38
49
  static set(key, data) {
39
- const value = typeof data === "string" ? data : JSON.stringify(data);
40
- localStorage.setItem(key, value);
50
+ this.attempt(() => {
51
+ const value = typeof data === "string" ? data : JSON.stringify(data);
52
+ localStorage.setItem(key, value);
53
+ }, void 0);
41
54
  }
42
55
  /** Retrieve and JSON-parse a value by key. Returns null if the key does not exist. */
43
56
  static get(key) {
44
- const dataStr = localStorage.getItem(key);
57
+ const dataStr = this.attempt(() => localStorage.getItem(key), null);
45
58
  if (!dataStr) return null;
46
59
  try {
47
60
  return JSON.parse(dataStr);
@@ -51,19 +64,19 @@ var LocalStore = class {
51
64
  }
52
65
  /** Remove a single key from localStorage. */
53
66
  static remove(key) {
54
- localStorage.removeItem(key);
67
+ this.attempt(() => localStorage.removeItem(key), void 0);
55
68
  }
56
69
  /** Clear all keys from localStorage. */
57
70
  static removeAll() {
58
- localStorage.clear();
71
+ this.attempt(() => localStorage.clear(), void 0);
59
72
  }
60
73
  /** Check whether a key exists in localStorage. */
61
74
  static has(key) {
62
- return localStorage.getItem(key) !== null;
75
+ return this.attempt(() => localStorage.getItem(key) !== null, false);
63
76
  }
64
77
  /** Return all keys currently stored in localStorage. */
65
78
  static keys() {
66
- return Object.keys(localStorage);
79
+ return this.attempt(() => Object.keys(localStorage), []);
67
80
  }
68
81
  /** Retrieve the stored access token. */
69
82
  static getToken() {
@@ -96,10 +109,92 @@ var LocalStore = class {
96
109
  this.remove(USER_KEY);
97
110
  }
98
111
  };
112
+
113
+ // src/download-blob.ts
114
+ function downloadBlob(blob, fileName, options = {}) {
115
+ if (typeof window === "undefined" || typeof document === "undefined") {
116
+ throw new Error("downloadBlob requires a browser");
117
+ }
118
+ const url = window.URL.createObjectURL(blob);
119
+ const link = document.createElement("a");
120
+ try {
121
+ link.href = url;
122
+ link.download = fileName;
123
+ document.body.appendChild(link);
124
+ link.click();
125
+ } finally {
126
+ link.remove();
127
+ if (options.revokeDelayMs && options.revokeDelayMs > 0) {
128
+ setTimeout(() => window.URL.revokeObjectURL(url), options.revokeDelayMs);
129
+ } else {
130
+ window.URL.revokeObjectURL(url);
131
+ }
132
+ }
133
+ }
134
+
135
+ // src/cookies.ts
136
+ function validateName(name) {
137
+ if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name)) {
138
+ throw new TypeError("Invalid cookie name");
139
+ }
140
+ }
141
+ function setCookie(name, value, options = {}) {
142
+ validateName(name);
143
+ const parts = [`${name}=${encodeURIComponent(value)}`];
144
+ if (options.path !== void 0) {
145
+ if (!options.path.startsWith("/") || /[;\x00-\x1f\x7f]/.test(options.path)) {
146
+ throw new TypeError("Invalid cookie path");
147
+ }
148
+ parts.push(`Path=${options.path}`);
149
+ }
150
+ if (options.domain !== void 0) {
151
+ if (!/^\.?[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i.test(options.domain)) {
152
+ throw new TypeError("Invalid cookie domain");
153
+ }
154
+ parts.push(`Domain=${options.domain}`);
155
+ }
156
+ if (options.expires !== void 0) {
157
+ if (!Number.isFinite(options.expires.getTime())) throw new TypeError("Invalid cookie expiry");
158
+ parts.push(`Expires=${options.expires.toUTCString()}`);
159
+ }
160
+ if (options.maxAge !== void 0) {
161
+ if (!Number.isSafeInteger(options.maxAge)) throw new TypeError("Invalid cookie max age");
162
+ parts.push(`Max-Age=${options.maxAge}`);
163
+ }
164
+ if (options.sameSite !== void 0) {
165
+ if (!["Strict", "Lax", "None"].includes(options.sameSite)) throw new TypeError("Invalid cookie SameSite");
166
+ parts.push(`SameSite=${options.sameSite}`);
167
+ }
168
+ if (options.secure) parts.push("Secure");
169
+ if (typeof document !== "undefined") document.cookie = parts.join("; ");
170
+ }
171
+ function getCookie(name) {
172
+ validateName(name);
173
+ if (typeof document === "undefined") return null;
174
+ const prefix = `${name}=`;
175
+ for (const entry of document.cookie.split(";")) {
176
+ const cookie = entry.trimStart();
177
+ if (!cookie.startsWith(prefix)) continue;
178
+ const value = cookie.slice(prefix.length);
179
+ try {
180
+ return decodeURIComponent(value);
181
+ } catch {
182
+ return value;
183
+ }
184
+ }
185
+ return null;
186
+ }
187
+ function removeCookie(name, options = {}) {
188
+ setCookie(name, "", { ...options, expires: /* @__PURE__ */ new Date(0), maxAge: 0 });
189
+ }
99
190
  // Annotate the CommonJS export names for ESM import in node:
100
191
  0 && (module.exports = {
101
192
  LocalStore,
102
193
  REFRESH_TOKEN_KEY,
103
194
  TOKEN_KEY,
104
- USER_KEY
195
+ USER_KEY,
196
+ downloadBlob,
197
+ getCookie,
198
+ removeCookie,
199
+ setCookie
105
200
  });
package/dist/index.mjs CHANGED
@@ -5,14 +5,23 @@ var REFRESH_TOKEN_KEY = "REFRESH_TOKEN";
5
5
 
6
6
  // src/local-store.ts
7
7
  var LocalStore = class {
8
+ static attempt(operation, fallback) {
9
+ try {
10
+ return operation();
11
+ } catch {
12
+ return fallback;
13
+ }
14
+ }
8
15
  /** Store a value under the given key. Non-string values are JSON-serialized. */
9
16
  static set(key, data) {
10
- const value = typeof data === "string" ? data : JSON.stringify(data);
11
- localStorage.setItem(key, value);
17
+ this.attempt(() => {
18
+ const value = typeof data === "string" ? data : JSON.stringify(data);
19
+ localStorage.setItem(key, value);
20
+ }, void 0);
12
21
  }
13
22
  /** Retrieve and JSON-parse a value by key. Returns null if the key does not exist. */
14
23
  static get(key) {
15
- const dataStr = localStorage.getItem(key);
24
+ const dataStr = this.attempt(() => localStorage.getItem(key), null);
16
25
  if (!dataStr) return null;
17
26
  try {
18
27
  return JSON.parse(dataStr);
@@ -22,19 +31,19 @@ var LocalStore = class {
22
31
  }
23
32
  /** Remove a single key from localStorage. */
24
33
  static remove(key) {
25
- localStorage.removeItem(key);
34
+ this.attempt(() => localStorage.removeItem(key), void 0);
26
35
  }
27
36
  /** Clear all keys from localStorage. */
28
37
  static removeAll() {
29
- localStorage.clear();
38
+ this.attempt(() => localStorage.clear(), void 0);
30
39
  }
31
40
  /** Check whether a key exists in localStorage. */
32
41
  static has(key) {
33
- return localStorage.getItem(key) !== null;
42
+ return this.attempt(() => localStorage.getItem(key) !== null, false);
34
43
  }
35
44
  /** Return all keys currently stored in localStorage. */
36
45
  static keys() {
37
- return Object.keys(localStorage);
46
+ return this.attempt(() => Object.keys(localStorage), []);
38
47
  }
39
48
  /** Retrieve the stored access token. */
40
49
  static getToken() {
@@ -67,9 +76,91 @@ var LocalStore = class {
67
76
  this.remove(USER_KEY);
68
77
  }
69
78
  };
79
+
80
+ // src/download-blob.ts
81
+ function downloadBlob(blob, fileName, options = {}) {
82
+ if (typeof window === "undefined" || typeof document === "undefined") {
83
+ throw new Error("downloadBlob requires a browser");
84
+ }
85
+ const url = window.URL.createObjectURL(blob);
86
+ const link = document.createElement("a");
87
+ try {
88
+ link.href = url;
89
+ link.download = fileName;
90
+ document.body.appendChild(link);
91
+ link.click();
92
+ } finally {
93
+ link.remove();
94
+ if (options.revokeDelayMs && options.revokeDelayMs > 0) {
95
+ setTimeout(() => window.URL.revokeObjectURL(url), options.revokeDelayMs);
96
+ } else {
97
+ window.URL.revokeObjectURL(url);
98
+ }
99
+ }
100
+ }
101
+
102
+ // src/cookies.ts
103
+ function validateName(name) {
104
+ if (!/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name)) {
105
+ throw new TypeError("Invalid cookie name");
106
+ }
107
+ }
108
+ function setCookie(name, value, options = {}) {
109
+ validateName(name);
110
+ const parts = [`${name}=${encodeURIComponent(value)}`];
111
+ if (options.path !== void 0) {
112
+ if (!options.path.startsWith("/") || /[;\x00-\x1f\x7f]/.test(options.path)) {
113
+ throw new TypeError("Invalid cookie path");
114
+ }
115
+ parts.push(`Path=${options.path}`);
116
+ }
117
+ if (options.domain !== void 0) {
118
+ if (!/^\.?[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i.test(options.domain)) {
119
+ throw new TypeError("Invalid cookie domain");
120
+ }
121
+ parts.push(`Domain=${options.domain}`);
122
+ }
123
+ if (options.expires !== void 0) {
124
+ if (!Number.isFinite(options.expires.getTime())) throw new TypeError("Invalid cookie expiry");
125
+ parts.push(`Expires=${options.expires.toUTCString()}`);
126
+ }
127
+ if (options.maxAge !== void 0) {
128
+ if (!Number.isSafeInteger(options.maxAge)) throw new TypeError("Invalid cookie max age");
129
+ parts.push(`Max-Age=${options.maxAge}`);
130
+ }
131
+ if (options.sameSite !== void 0) {
132
+ if (!["Strict", "Lax", "None"].includes(options.sameSite)) throw new TypeError("Invalid cookie SameSite");
133
+ parts.push(`SameSite=${options.sameSite}`);
134
+ }
135
+ if (options.secure) parts.push("Secure");
136
+ if (typeof document !== "undefined") document.cookie = parts.join("; ");
137
+ }
138
+ function getCookie(name) {
139
+ validateName(name);
140
+ if (typeof document === "undefined") return null;
141
+ const prefix = `${name}=`;
142
+ for (const entry of document.cookie.split(";")) {
143
+ const cookie = entry.trimStart();
144
+ if (!cookie.startsWith(prefix)) continue;
145
+ const value = cookie.slice(prefix.length);
146
+ try {
147
+ return decodeURIComponent(value);
148
+ } catch {
149
+ return value;
150
+ }
151
+ }
152
+ return null;
153
+ }
154
+ function removeCookie(name, options = {}) {
155
+ setCookie(name, "", { ...options, expires: /* @__PURE__ */ new Date(0), maxAge: 0 });
156
+ }
70
157
  export {
71
158
  LocalStore,
72
159
  REFRESH_TOKEN_KEY,
73
160
  TOKEN_KEY,
74
- USER_KEY
161
+ USER_KEY,
162
+ downloadBlob,
163
+ getCookie,
164
+ removeCookie,
165
+ setCookie
75
166
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zucker-framework/fe-storage",
3
- "version": "1.0.0",
3
+ "version": "1.0.5",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",