@fluixi/storage 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/dist/adapters/memory.cjs +46 -0
  4. package/dist/adapters/memory.d.ts +8 -0
  5. package/dist/adapters/memory.d.ts.map +1 -0
  6. package/dist/adapters/memory.js +26 -0
  7. package/dist/adapters/memory.mjs +25 -0
  8. package/dist/browser/document-cookie.cjs +101 -0
  9. package/dist/browser/document-cookie.d.ts +14 -0
  10. package/dist/browser/document-cookie.d.ts.map +1 -0
  11. package/dist/browser/document-cookie.js +53 -0
  12. package/dist/browser/document-cookie.mjs +80 -0
  13. package/dist/browser/index.cjs +209 -0
  14. package/dist/browser/index.d.ts +4 -0
  15. package/dist/browser/index.d.ts.map +1 -0
  16. package/dist/browser/index.js +3 -0
  17. package/dist/browser/index.mjs +187 -0
  18. package/dist/browser/web-storage.cjs +150 -0
  19. package/dist/browser/web-storage.d.ts +6 -0
  20. package/dist/browser/web-storage.d.ts.map +1 -0
  21. package/dist/browser/web-storage.js +105 -0
  22. package/dist/browser/web-storage.mjs +127 -0
  23. package/dist/core/async-storage.cjs +583 -0
  24. package/dist/core/async-storage.d.ts +17 -0
  25. package/dist/core/async-storage.d.ts.map +1 -0
  26. package/dist/core/async-storage.js +275 -0
  27. package/dist/core/async-storage.mjs +560 -0
  28. package/dist/core/bus.cjs +52 -0
  29. package/dist/core/bus.d.ts +19 -0
  30. package/dist/core/bus.d.ts.map +1 -0
  31. package/dist/core/bus.js +31 -0
  32. package/dist/core/bus.mjs +31 -0
  33. package/dist/core/envelope.cjs +119 -0
  34. package/dist/core/envelope.d.ts +38 -0
  35. package/dist/core/envelope.d.ts.map +1 -0
  36. package/dist/core/envelope.js +83 -0
  37. package/dist/core/envelope.mjs +96 -0
  38. package/dist/core/errors.cjs +115 -0
  39. package/dist/core/errors.d.ts +109 -0
  40. package/dist/core/errors.d.ts.map +1 -0
  41. package/dist/core/errors.js +99 -0
  42. package/dist/core/errors.mjs +94 -0
  43. package/dist/core/storage.cjs +782 -0
  44. package/dist/core/storage.d.ts +63 -0
  45. package/dist/core/storage.d.ts.map +1 -0
  46. package/dist/core/storage.js +260 -0
  47. package/dist/core/storage.mjs +759 -0
  48. package/dist/core/types.cjs +18 -0
  49. package/dist/core/types.d.ts +239 -0
  50. package/dist/core/types.d.ts.map +1 -0
  51. package/dist/core/types.js +1 -0
  52. package/dist/core/types.mjs +0 -0
  53. package/dist/encryption/base64.cjs +38 -0
  54. package/dist/encryption/base64.d.ts +4 -0
  55. package/dist/encryption/base64.d.ts.map +1 -0
  56. package/dist/encryption/base64.js +16 -0
  57. package/dist/encryption/base64.mjs +17 -0
  58. package/dist/encryption/keys.cjs +77 -0
  59. package/dist/encryption/keys.d.ts +14 -0
  60. package/dist/encryption/keys.d.ts.map +1 -0
  61. package/dist/encryption/keys.js +28 -0
  62. package/dist/encryption/keys.mjs +54 -0
  63. package/dist/encryption/seal.cjs +154 -0
  64. package/dist/encryption/seal.d.ts +24 -0
  65. package/dist/encryption/seal.d.ts.map +1 -0
  66. package/dist/encryption/seal.js +83 -0
  67. package/dist/encryption/seal.mjs +131 -0
  68. package/dist/encryption/types.cjs +18 -0
  69. package/dist/encryption/types.d.ts +50 -0
  70. package/dist/encryption/types.d.ts.map +1 -0
  71. package/dist/encryption/types.js +1 -0
  72. package/dist/encryption/types.mjs +0 -0
  73. package/dist/encryption/web-crypto.cjs +81 -0
  74. package/dist/encryption/web-crypto.d.ts +9 -0
  75. package/dist/encryption/web-crypto.d.ts.map +1 -0
  76. package/dist/encryption/web-crypto.js +45 -0
  77. package/dist/encryption/web-crypto.mjs +58 -0
  78. package/dist/index.cjs +870 -0
  79. package/dist/index.d.ts +16 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +13 -0
  82. package/dist/index.mjs +848 -0
  83. package/dist/serialization/json.cjs +83 -0
  84. package/dist/serialization/json.d.ts +3 -0
  85. package/dist/serialization/json.d.ts.map +1 -0
  86. package/dist/serialization/json.js +51 -0
  87. package/dist/serialization/json.mjs +60 -0
  88. package/dist/serialization/serializer.cjs +28 -0
  89. package/dist/serialization/serializer.d.ts +4 -0
  90. package/dist/serialization/serializer.d.ts.map +1 -0
  91. package/dist/serialization/serializer.js +4 -0
  92. package/dist/serialization/serializer.mjs +7 -0
  93. package/dist/server/contracts.cjs +36 -0
  94. package/dist/server/contracts.d.ts +27 -0
  95. package/dist/server/contracts.d.ts.map +1 -0
  96. package/dist/server/contracts.js +16 -0
  97. package/dist/server/contracts.mjs +15 -0
  98. package/dist/server/cookie.cjs +126 -0
  99. package/dist/server/cookie.d.ts +21 -0
  100. package/dist/server/cookie.d.ts.map +1 -0
  101. package/dist/server/cookie.js +71 -0
  102. package/dist/server/cookie.mjs +105 -0
  103. package/dist/server/index.cjs +155 -0
  104. package/dist/server/index.d.ts +5 -0
  105. package/dist/server/index.d.ts.map +1 -0
  106. package/dist/server/index.js +4 -0
  107. package/dist/server/index.mjs +133 -0
  108. package/dist/server/request.cjs +46 -0
  109. package/dist/server/request.d.ts +7 -0
  110. package/dist/server/request.d.ts.map +1 -0
  111. package/dist/server/request.js +25 -0
  112. package/dist/server/request.mjs +25 -0
  113. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  114. package/dist/utils/cookie-names.cjs +38 -0
  115. package/dist/utils/cookie-names.d.ts +10 -0
  116. package/dist/utils/cookie-names.d.ts.map +1 -0
  117. package/dist/utils/cookie-names.js +15 -0
  118. package/dist/utils/cookie-names.mjs +17 -0
  119. package/dist/utils/copy.cjs +38 -0
  120. package/dist/utils/copy.d.ts +17 -0
  121. package/dist/utils/copy.d.ts.map +1 -0
  122. package/dist/utils/copy.js +15 -0
  123. package/dist/utils/copy.mjs +17 -0
  124. package/dist/utils/environment.cjs +28 -0
  125. package/dist/utils/environment.d.ts +3 -0
  126. package/dist/utils/environment.d.ts.map +1 -0
  127. package/dist/utils/environment.js +4 -0
  128. package/dist/utils/environment.mjs +7 -0
  129. package/dist/utils/keys.cjs +33 -0
  130. package/dist/utils/keys.d.ts +5 -0
  131. package/dist/utils/keys.d.ts.map +1 -0
  132. package/dist/utils/keys.js +10 -0
  133. package/dist/utils/keys.mjs +12 -0
  134. package/package.json +61 -0
@@ -0,0 +1,275 @@
1
+ /**
2
+ * The awaited storage. Same envelope as the sync one, so both read each other's plaintext
3
+ * entries; it also works over async adapters and is where per-item encryption runs.
4
+ */
5
+ import { memoryStorage } from '../adapters/memory.js';
6
+ import { open, seal } from '../encryption/seal.js';
7
+ import { webCryptoProvider } from '../encryption/web-crypto.js';
8
+ import { jsonSerializer } from '../serialization/json.js';
9
+ import { joinNamespace, namespacePrefix } from '../utils/keys.js';
10
+ import { decodeEnvelope, deserializeOrThrow, isExpired, latestVersion, metaFor, migrate, selectItems, serializeOrThrow, toEntry, } from './envelope.js';
11
+ import { joinBus, publish } from './bus.js';
12
+ import { StorageEncryptionKeyError } from './errors.js';
13
+ /**
14
+ * A typed storage whose operations all return promises. It works over a synchronous adapter or
15
+ * an asynchronous one (IndexedDB, a remote store), and it is the storage that can encrypt.
16
+ *
17
+ * It reads and writes the same envelope as the synchronous storage, so the two can share an
18
+ * adapter. Without `encryption`, writing with `{ encrypt: true }` or reading an encrypted item
19
+ * throws `StorageEncryptionKeyError`.
20
+ *
21
+ * @param options As for `createStorage`, plus `encryption` and an asynchronous `adapter`.
22
+ *
23
+ * @example
24
+ * const storage = createAsyncStorage<{ token: string }>({ encryption: { keyProvider } });
25
+ * await storage.set('token', token, { encrypt: true, ttl: 3_600_000 });
26
+ */
27
+ export function createAsyncStorage(options = {}) {
28
+ const adapter = options.adapter ?? memoryStorage();
29
+ const serializer = options.serializer ?? jsonSerializer;
30
+ const now = options.now ?? Date.now;
31
+ const migrations = (options.migrations ?? {});
32
+ const prefix = namespacePrefix(options.namespace);
33
+ const listeners = new Set();
34
+ let leave;
35
+ // Without `encryption` a provider is still there, so reading an encrypted entry says the key is missing.
36
+ const sealer = {
37
+ keyProvider: options.encryption?.keyProvider,
38
+ provider: options.encryption?.provider ?? webCryptoProvider(),
39
+ };
40
+ const context = (key) => (options.namespace ? { key, namespace: options.namespace } : { key });
41
+ const deliver = (event) => {
42
+ for (const { key, fn } of [...listeners])
43
+ if (key === undefined || event.key === null || key === event.key)
44
+ fn(event);
45
+ };
46
+ // Through the adapter's bus, so every storage over it hears the write, this one included.
47
+ const emit = (event) => publish(adapter, event.type, event.key === null ? null : prefix + event.key, prefix, event.value);
48
+ const store = async (key, value, meta, encrypt) => {
49
+ const envelope = { v: 1, ...meta };
50
+ if (encrypt) {
51
+ if (!sealer.keyProvider)
52
+ throw new StorageEncryptionKeyError(`Encrypting ${key} needs encryption on the storage.`, { key });
53
+ envelope.enc = await seal(sealer, context(key), prefix + key, serializeOrThrow(serializer, key, value), meta);
54
+ }
55
+ else {
56
+ envelope.value = value;
57
+ }
58
+ await adapter.set(prefix + key, serializeOrThrow(serializer, key, envelope), { expiresAt: meta.expiresAt });
59
+ };
60
+ const write = async (key, value, meta, encrypt) => {
61
+ await store(key, value, meta, encrypt);
62
+ emit({ type: 'set', key: key, value, external: false });
63
+ };
64
+ const metaOf = ({ createdAt, expiresAt, version }) => {
65
+ const meta = { createdAt };
66
+ if (expiresAt !== undefined)
67
+ meta.expiresAt = expiresAt;
68
+ if (version !== undefined)
69
+ meta.version = version;
70
+ return meta;
71
+ };
72
+ /** The live entry and its plaintext value, expiry applied and migrations run, or null. */
73
+ const read = async (key) => {
74
+ const text = await adapter.get(prefix + key);
75
+ if (text === null)
76
+ return null;
77
+ let envelope = decodeEnvelope(serializer, key, text);
78
+ if (isExpired(envelope, now())) {
79
+ await adapter.remove(prefix + key);
80
+ emit({ type: 'expire', key: key, external: false });
81
+ return null;
82
+ }
83
+ let value = envelope.enc
84
+ ? deserializeOrThrow(serializer, key, await open(sealer, context(key), prefix + key, envelope.enc, metaOf(envelope)))
85
+ : envelope.value;
86
+ const migrated = migrate(migrations[key], envelope.version, value);
87
+ if (migrated) {
88
+ value = migrated.value;
89
+ const meta = { ...metaOf(envelope), version: migrated.version };
90
+ // Encrypted again under a fresh IV: the version is part of what is authenticated.
91
+ await store(key, value, meta, !!envelope.enc);
92
+ envelope = { v: 1, ...meta, ...(envelope.enc ? { enc: envelope.enc } : { value }) };
93
+ }
94
+ return { envelope, value };
95
+ };
96
+ const ownKeys = async () => (await adapter.keys()).filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length));
97
+ const external = (change) => {
98
+ if (change.key === null)
99
+ return deliver({ type: 'clear', key: null, external: true });
100
+ if (!change.key.startsWith(prefix))
101
+ return;
102
+ const key = change.key.slice(prefix.length);
103
+ if (change.value === null)
104
+ return deliver({ type: 'remove', key, external: true });
105
+ try {
106
+ const envelope = decodeEnvelope(serializer, key, change.value);
107
+ deliver({ type: 'set', key, value: envelope.enc ? undefined : envelope.value, external: true });
108
+ }
109
+ catch {
110
+ // Unreadable text from another writer: reported when it is read, not here.
111
+ }
112
+ };
113
+ const liveItems = async () => {
114
+ const items = [];
115
+ for (const key of (await ownKeys()).sort()) {
116
+ const current = await read(key);
117
+ if (current)
118
+ items.push({ key: key, value: current.value, entry: toEntry(current.envelope, current.value) });
119
+ }
120
+ return items;
121
+ };
122
+ const storage = {
123
+ scope: adapter.scope,
124
+ keyPrefix: prefix,
125
+ capabilities: { ...adapter.capabilities, expiration: true },
126
+ encryption: !!sealer.keyProvider,
127
+ async get(key) {
128
+ return ((await read(key))?.value ?? null);
129
+ },
130
+ async getEntry(key) {
131
+ const current = await read(key);
132
+ return current ? toEntry(current.envelope, current.value) : null;
133
+ },
134
+ async set(key, value, setOptions = {}) {
135
+ const meta = metaFor(now(), setOptions.ttl, setOptions.version ?? latestVersion(migrations[key]));
136
+ await write(key, value, meta, setOptions.encrypt === true);
137
+ },
138
+ entries: liveItems,
139
+ async search(query) {
140
+ return selectItems(await liveItems(), query);
141
+ },
142
+ async size() {
143
+ let count = 0;
144
+ for (const key of await ownKeys()) {
145
+ const text = await adapter.get(prefix + key);
146
+ if (text !== null && !isExpired(decodeEnvelope(serializer, key, text), now()))
147
+ count++;
148
+ }
149
+ return count;
150
+ },
151
+ async prune() {
152
+ let removed = 0;
153
+ for (const key of await ownKeys()) {
154
+ const text = await adapter.get(prefix + key);
155
+ if (text === null)
156
+ continue;
157
+ if (isExpired(decodeEnvelope(serializer, key, text), now())) {
158
+ await adapter.remove(prefix + key);
159
+ emit({ type: 'expire', key: key, external: false });
160
+ removed++;
161
+ }
162
+ }
163
+ return removed;
164
+ },
165
+ async usage() {
166
+ let bytes = 0;
167
+ const keys = await ownKeys();
168
+ for (const key of keys)
169
+ bytes += ((prefix + key).length + ((await adapter.get(prefix + key))?.length ?? 0)) * 2;
170
+ return { keys: keys.length, bytes };
171
+ },
172
+ async update(key, fn, updateOptions = {}) {
173
+ const current = await read(key);
174
+ const value = await fn((current?.value ?? null));
175
+ const meta = metaFor(now(), undefined, current?.envelope.version ?? latestVersion(migrations[key]));
176
+ if (current?.envelope.expiresAt !== undefined)
177
+ meta.expiresAt = current.envelope.expiresAt;
178
+ await write(key, value, meta, updateOptions.encrypt ?? !!current?.envelope.enc);
179
+ return value;
180
+ },
181
+ async getOrSet(key, factory, setOptions) {
182
+ const current = await read(key);
183
+ if (current)
184
+ return current.value;
185
+ const value = await factory();
186
+ await storage.set(key, value, setOptions);
187
+ return value;
188
+ },
189
+ async touch(key, ttl) {
190
+ const current = await read(key);
191
+ if (!current)
192
+ return false;
193
+ await store(key, current.value, { ...metaOf(current.envelope), expiresAt: now() + ttl }, !!current.envelope.enc);
194
+ return true;
195
+ },
196
+ async snapshot() {
197
+ const out = {};
198
+ for (const item of await liveItems())
199
+ out[item.key] = item.entry;
200
+ return out;
201
+ },
202
+ async restore(snapshot, restoreOptions = {}) {
203
+ let written = 0;
204
+ for (const [key, entry] of Object.entries(snapshot)) {
205
+ if (!entry)
206
+ continue;
207
+ if (!restoreOptions.overwrite && (await read(key)))
208
+ continue;
209
+ if (entry.expiresAt !== undefined && entry.expiresAt <= now())
210
+ continue;
211
+ const meta = { createdAt: entry.createdAt };
212
+ if (entry.expiresAt !== undefined)
213
+ meta.expiresAt = entry.expiresAt;
214
+ if (entry.version !== undefined)
215
+ meta.version = entry.version;
216
+ await write(key, entry.value, meta, entry.encrypted === true);
217
+ written++;
218
+ }
219
+ return written;
220
+ },
221
+ async remove(key) {
222
+ await adapter.remove(prefix + key);
223
+ emit({ type: 'remove', key, external: false });
224
+ },
225
+ async has(key) {
226
+ const text = await adapter.get(prefix + key);
227
+ if (text === null)
228
+ return false;
229
+ if (!isExpired(decodeEnvelope(serializer, key, text), now()))
230
+ return true;
231
+ await adapter.remove(prefix + key);
232
+ emit({ type: 'expire', key, external: false });
233
+ return false;
234
+ },
235
+ async keys() {
236
+ return (await ownKeys());
237
+ },
238
+ async clear() {
239
+ if (prefix)
240
+ for (const key of await ownKeys())
241
+ await adapter.remove(prefix + key);
242
+ else
243
+ await adapter.clear();
244
+ emit({ type: 'clear', key: null, external: false });
245
+ },
246
+ namespace(name) {
247
+ return createAsyncStorage({ ...options, migrations: undefined, namespace: joinNamespace(options.namespace, name) });
248
+ },
249
+ subscribe(keyOrListener, maybeListener) {
250
+ const entry = typeof keyOrListener === 'function'
251
+ ? { fn: keyOrListener }
252
+ : { key: keyOrListener, fn: maybeListener };
253
+ listeners.add(entry);
254
+ if (listeners.size === 1) {
255
+ leave = joinBus(adapter, {
256
+ prefix,
257
+ external,
258
+ local(type, key, value) {
259
+ const event = { type, key: key === null ? null : key.slice(prefix.length), external: false };
260
+ if (type === 'set')
261
+ event.value = value;
262
+ deliver(event);
263
+ },
264
+ });
265
+ }
266
+ return () => {
267
+ if (listeners.delete(entry) && listeners.size === 0) {
268
+ leave?.();
269
+ leave = undefined;
270
+ }
271
+ };
272
+ },
273
+ };
274
+ return storage;
275
+ }