@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,154 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/encryption/seal.ts
21
+ var seal_exports = {};
22
+ __export(seal_exports, {
23
+ PAYLOAD_VERSION: () => PAYLOAD_VERSION,
24
+ open: () => open,
25
+ seal: () => seal
26
+ });
27
+ module.exports = __toCommonJS(seal_exports);
28
+
29
+ // src/core/errors.ts
30
+ var StorageError = class extends Error {
31
+ constructor(code, message, options = {}) {
32
+ super(message);
33
+ this.name = new.target.name;
34
+ this.code = code;
35
+ this.key = options.key;
36
+ if (options.cause !== void 0) this.cause = options.cause;
37
+ }
38
+ };
39
+ var StorageEncryptionError = class extends StorageError {
40
+ };
41
+ var StorageEncryptionKeyError = class extends StorageEncryptionError {
42
+ constructor(message, options = {}) {
43
+ super("encryption-key", message, options);
44
+ }
45
+ };
46
+ var StorageEncryptionMetadataError = class extends StorageEncryptionError {
47
+ constructor(message, options = {}) {
48
+ super("encryption-metadata", message, options);
49
+ }
50
+ };
51
+ var StorageEncryptionVersionError = class extends StorageEncryptionError {
52
+ constructor(message, options = {}) {
53
+ super("encryption-version", message, options);
54
+ }
55
+ };
56
+ var StorageEncryptionAlgorithmError = class extends StorageEncryptionError {
57
+ constructor(message, options = {}) {
58
+ super("encryption-algorithm", message, options);
59
+ }
60
+ };
61
+ var StorageDecryptionError = class extends StorageEncryptionError {
62
+ constructor(message, options = {}) {
63
+ super("decryption", message, options);
64
+ }
65
+ };
66
+ var StorageEncryptedPayloadError = class extends StorageEncryptionError {
67
+ constructor(message, options = {}) {
68
+ super("encryption-payload", message, options);
69
+ }
70
+ };
71
+
72
+ // src/encryption/base64.ts
73
+ function toBase64(bytes) {
74
+ let binary = "";
75
+ for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
76
+ return btoa(binary);
77
+ }
78
+ function fromBase64(text) {
79
+ if (text.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(text)) throw new TypeError("Not base64.");
80
+ const binary = atob(text);
81
+ const out = new Uint8Array(binary.length);
82
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
83
+ return out;
84
+ }
85
+
86
+ // src/encryption/seal.ts
87
+ var PAYLOAD_VERSION = 1;
88
+ var encoder = new TextEncoder();
89
+ var decoder = new TextDecoder("utf-8", { fatal: true });
90
+ var aad = (storedKey, alg, kid, meta) => encoder.encode(JSON.stringify(["fx-storage", PAYLOAD_VERSION, alg, storedKey, meta.createdAt, meta.expiresAt ?? null, meta.version ?? null, kid ?? null]));
91
+ async function keyFor(sealer, context) {
92
+ if (!sealer.keyProvider) {
93
+ throw new StorageEncryptionKeyError(`${context.key} is encrypted, and this storage has no encryption key provider.`, { key: context.key });
94
+ }
95
+ let result;
96
+ try {
97
+ result = await sealer.keyProvider.getKey(context);
98
+ } catch (error) {
99
+ if (error instanceof StorageEncryptionError) throw error;
100
+ throw new StorageEncryptionKeyError(`The key provider failed for ${context.key}.`, { key: context.key, cause: error });
101
+ }
102
+ const pair = result && "key" in result && "id" in result ? result : { key: result };
103
+ const algorithm = pair.key?.algorithm;
104
+ if (!algorithm || algorithm.name !== "AES-GCM" || algorithm.length !== 256) {
105
+ throw new StorageEncryptionKeyError(`The key provider must return an AES-GCM 256-bit key for ${context.key}.`, { key: context.key });
106
+ }
107
+ if (!pair.key.usages.includes(context.operation)) {
108
+ throw new StorageEncryptionKeyError(`The key for ${context.key} does not allow ${context.operation}.`, { key: context.key });
109
+ }
110
+ return pair;
111
+ }
112
+ async function seal(sealer, context, storedKey, plaintext, meta) {
113
+ const { key, id } = await keyFor(sealer, { ...context, operation: "encrypt" });
114
+ const alg = sealer.provider.algorithm;
115
+ const out = await sealer.provider.encrypt(encoder.encode(plaintext), key, aad(storedKey, alg, id, meta));
116
+ const payload = { v: PAYLOAD_VERSION, alg, iv: toBase64(out.iv), data: toBase64(out.ciphertext), tag: toBase64(out.tag) };
117
+ if (id !== void 0) payload.kid = id;
118
+ return payload;
119
+ }
120
+ async function open(sealer, context, storedKey, payload, meta) {
121
+ const k = context.key;
122
+ const p = payload;
123
+ if (!p || typeof p !== "object" || typeof p.v !== "number") {
124
+ throw new StorageEncryptionMetadataError(`The encryption metadata of ${k} is missing its version.`, { key: k });
125
+ }
126
+ if (p.v !== PAYLOAD_VERSION) throw new StorageEncryptionVersionError(`${k} uses encryption format ${p.v}, which this release cannot read.`, { key: k });
127
+ if (typeof p.alg !== "string" || typeof p.iv !== "string" || typeof p.data !== "string" || typeof p.tag !== "string" || p.kid !== void 0 && typeof p.kid !== "string") {
128
+ throw new StorageEncryptionMetadataError(`The encryption metadata of ${k} is malformed.`, { key: k });
129
+ }
130
+ if (p.alg !== sealer.provider.algorithm) {
131
+ throw new StorageEncryptionAlgorithmError(`${k} is encrypted with ${p.alg}; the provider implements ${sealer.provider.algorithm}.`, { key: k });
132
+ }
133
+ let iv, ciphertext, tag;
134
+ try {
135
+ iv = fromBase64(p.iv);
136
+ ciphertext = fromBase64(p.data);
137
+ tag = fromBase64(p.tag);
138
+ } catch (error) {
139
+ throw new StorageEncryptedPayloadError(`The encrypted payload of ${k} is not valid base64.`, { key: k, cause: error });
140
+ }
141
+ if (iv.length !== 12 || tag.length !== 16) throw new StorageEncryptedPayloadError(`The encrypted payload of ${k} has a wrong IV or tag length.`, { key: k });
142
+ const { key } = await keyFor(sealer, { ...context, operation: "decrypt", keyId: p.kid });
143
+ let bytes;
144
+ try {
145
+ bytes = await sealer.provider.decrypt({ algorithm: p.alg, iv, ciphertext, tag }, key, aad(storedKey, p.alg, p.kid, meta));
146
+ } catch (error) {
147
+ throw new StorageDecryptionError(`${k} failed authentication: wrong key, or the stored entry was changed.`, { key: k, cause: error });
148
+ }
149
+ try {
150
+ return decoder.decode(bytes);
151
+ } catch (error) {
152
+ throw new StorageEncryptedPayloadError(`The decrypted value of ${k} is not UTF-8.`, { key: k, cause: error });
153
+ }
154
+ }
@@ -0,0 +1,24 @@
1
+ import type { CryptoProvider, EncryptionContext, EncryptionKeyProvider } from './types.js';
2
+ export declare const PAYLOAD_VERSION = 1;
3
+ /** What an encrypted entry stores in place of its value. */
4
+ export interface EncryptedPayload {
5
+ v: number;
6
+ alg: string;
7
+ iv: string;
8
+ data: string;
9
+ tag: string;
10
+ kid?: string;
11
+ }
12
+ /** Metadata stored in clear beside the payload. Authenticated, so editing it fails decryption. */
13
+ export interface SealMeta {
14
+ createdAt: number;
15
+ expiresAt?: number;
16
+ version?: number;
17
+ }
18
+ export interface Sealer {
19
+ keyProvider?: EncryptionKeyProvider;
20
+ provider: CryptoProvider;
21
+ }
22
+ export declare function seal(sealer: Sealer, context: Omit<EncryptionContext, 'operation' | 'keyId'>, storedKey: string, plaintext: string, meta: SealMeta): Promise<EncryptedPayload>;
23
+ export declare function open(sealer: Sealer, context: Omit<EncryptionContext, 'operation' | 'keyId'>, storedKey: string, payload: unknown, meta: SealMeta): Promise<string>;
24
+ //# sourceMappingURL=seal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seal.d.ts","sourceRoot":"","sources":["../../src/encryption/seal.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAE3F,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,MAAM,CAAC;IACV,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,kGAAkG;AAClG,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,MAAM;IACrB,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,QAAQ,EAAE,cAAc,CAAC;CAC1B;AA+BD,wBAAsB,IAAI,CACxB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,OAAO,CAAC,EACvD,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,gBAAgB,CAAC,CAO3B;AAED,wBAAsB,IAAI,CACxB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,OAAO,CAAC,EACvD,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,MAAM,CAAC,CAkCjB"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Encrypts serialized values into the stored payload and back. The payload is versioned on its
3
+ * own, apart from the envelope, so the scheme can change without touching plaintext entries.
4
+ */
5
+ import { StorageDecryptionError, StorageEncryptedPayloadError, StorageEncryptionAlgorithmError, StorageEncryptionError, StorageEncryptionKeyError, StorageEncryptionMetadataError, StorageEncryptionVersionError, } from '../core/errors.js';
6
+ import { fromBase64, toBase64 } from './base64.js';
7
+ export const PAYLOAD_VERSION = 1;
8
+ const encoder = new TextEncoder();
9
+ const decoder = new TextDecoder('utf-8', { fatal: true });
10
+ // The full stored key is bound, so a payload copied under another key does not decrypt.
11
+ const aad = (storedKey, alg, kid, meta) => encoder.encode(JSON.stringify(['fx-storage', PAYLOAD_VERSION, alg, storedKey, meta.createdAt, meta.expiresAt ?? null, meta.version ?? null, kid ?? null]));
12
+ async function keyFor(sealer, context) {
13
+ if (!sealer.keyProvider) {
14
+ throw new StorageEncryptionKeyError(`${context.key} is encrypted, and this storage has no encryption key provider.`, { key: context.key });
15
+ }
16
+ let result;
17
+ try {
18
+ result = await sealer.keyProvider.getKey(context);
19
+ }
20
+ catch (error) {
21
+ if (error instanceof StorageEncryptionError)
22
+ throw error;
23
+ throw new StorageEncryptionKeyError(`The key provider failed for ${context.key}.`, { key: context.key, cause: error });
24
+ }
25
+ const pair = result && 'key' in result && 'id' in result ? result : { key: result };
26
+ const algorithm = pair.key?.algorithm;
27
+ if (!algorithm || algorithm.name !== 'AES-GCM' || algorithm.length !== 256) {
28
+ throw new StorageEncryptionKeyError(`The key provider must return an AES-GCM 256-bit key for ${context.key}.`, { key: context.key });
29
+ }
30
+ if (!pair.key.usages.includes(context.operation)) {
31
+ throw new StorageEncryptionKeyError(`The key for ${context.key} does not allow ${context.operation}.`, { key: context.key });
32
+ }
33
+ return pair;
34
+ }
35
+ export async function seal(sealer, context, storedKey, plaintext, meta) {
36
+ const { key, id } = await keyFor(sealer, { ...context, operation: 'encrypt' });
37
+ const alg = sealer.provider.algorithm;
38
+ const out = await sealer.provider.encrypt(encoder.encode(plaintext), key, aad(storedKey, alg, id, meta));
39
+ const payload = { v: PAYLOAD_VERSION, alg, iv: toBase64(out.iv), data: toBase64(out.ciphertext), tag: toBase64(out.tag) };
40
+ if (id !== undefined)
41
+ payload.kid = id;
42
+ return payload;
43
+ }
44
+ export async function open(sealer, context, storedKey, payload, meta) {
45
+ const k = context.key;
46
+ const p = payload;
47
+ if (!p || typeof p !== 'object' || typeof p.v !== 'number') {
48
+ throw new StorageEncryptionMetadataError(`The encryption metadata of ${k} is missing its version.`, { key: k });
49
+ }
50
+ if (p.v !== PAYLOAD_VERSION)
51
+ throw new StorageEncryptionVersionError(`${k} uses encryption format ${p.v}, which this release cannot read.`, { key: k });
52
+ if (typeof p.alg !== 'string' || typeof p.iv !== 'string' || typeof p.data !== 'string' || typeof p.tag !== 'string' || (p.kid !== undefined && typeof p.kid !== 'string')) {
53
+ throw new StorageEncryptionMetadataError(`The encryption metadata of ${k} is malformed.`, { key: k });
54
+ }
55
+ if (p.alg !== sealer.provider.algorithm) {
56
+ throw new StorageEncryptionAlgorithmError(`${k} is encrypted with ${p.alg}; the provider implements ${sealer.provider.algorithm}.`, { key: k });
57
+ }
58
+ let iv, ciphertext, tag;
59
+ try {
60
+ iv = fromBase64(p.iv);
61
+ ciphertext = fromBase64(p.data);
62
+ tag = fromBase64(p.tag);
63
+ }
64
+ catch (error) {
65
+ throw new StorageEncryptedPayloadError(`The encrypted payload of ${k} is not valid base64.`, { key: k, cause: error });
66
+ }
67
+ if (iv.length !== 12 || tag.length !== 16)
68
+ throw new StorageEncryptedPayloadError(`The encrypted payload of ${k} has a wrong IV or tag length.`, { key: k });
69
+ const { key } = await keyFor(sealer, { ...context, operation: 'decrypt', keyId: p.kid });
70
+ let bytes;
71
+ try {
72
+ bytes = await sealer.provider.decrypt({ algorithm: p.alg, iv, ciphertext, tag }, key, aad(storedKey, p.alg, p.kid, meta));
73
+ }
74
+ catch (error) {
75
+ throw new StorageDecryptionError(`${k} failed authentication: wrong key, or the stored entry was changed.`, { key: k, cause: error });
76
+ }
77
+ try {
78
+ return decoder.decode(bytes);
79
+ }
80
+ catch (error) {
81
+ throw new StorageEncryptedPayloadError(`The decrypted value of ${k} is not UTF-8.`, { key: k, cause: error });
82
+ }
83
+ }
@@ -0,0 +1,131 @@
1
+ // src/core/errors.ts
2
+ var StorageError = class extends Error {
3
+ constructor(code, message, options = {}) {
4
+ super(message);
5
+ this.name = new.target.name;
6
+ this.code = code;
7
+ this.key = options.key;
8
+ if (options.cause !== void 0) this.cause = options.cause;
9
+ }
10
+ };
11
+ var StorageEncryptionError = class extends StorageError {
12
+ };
13
+ var StorageEncryptionKeyError = class extends StorageEncryptionError {
14
+ constructor(message, options = {}) {
15
+ super("encryption-key", message, options);
16
+ }
17
+ };
18
+ var StorageEncryptionMetadataError = class extends StorageEncryptionError {
19
+ constructor(message, options = {}) {
20
+ super("encryption-metadata", message, options);
21
+ }
22
+ };
23
+ var StorageEncryptionVersionError = class extends StorageEncryptionError {
24
+ constructor(message, options = {}) {
25
+ super("encryption-version", message, options);
26
+ }
27
+ };
28
+ var StorageEncryptionAlgorithmError = class extends StorageEncryptionError {
29
+ constructor(message, options = {}) {
30
+ super("encryption-algorithm", message, options);
31
+ }
32
+ };
33
+ var StorageDecryptionError = class extends StorageEncryptionError {
34
+ constructor(message, options = {}) {
35
+ super("decryption", message, options);
36
+ }
37
+ };
38
+ var StorageEncryptedPayloadError = class extends StorageEncryptionError {
39
+ constructor(message, options = {}) {
40
+ super("encryption-payload", message, options);
41
+ }
42
+ };
43
+
44
+ // src/encryption/base64.ts
45
+ function toBase64(bytes) {
46
+ let binary = "";
47
+ for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
48
+ return btoa(binary);
49
+ }
50
+ function fromBase64(text) {
51
+ if (text.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(text)) throw new TypeError("Not base64.");
52
+ const binary = atob(text);
53
+ const out = new Uint8Array(binary.length);
54
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
55
+ return out;
56
+ }
57
+
58
+ // src/encryption/seal.ts
59
+ var PAYLOAD_VERSION = 1;
60
+ var encoder = new TextEncoder();
61
+ var decoder = new TextDecoder("utf-8", { fatal: true });
62
+ var aad = (storedKey, alg, kid, meta) => encoder.encode(JSON.stringify(["fx-storage", PAYLOAD_VERSION, alg, storedKey, meta.createdAt, meta.expiresAt ?? null, meta.version ?? null, kid ?? null]));
63
+ async function keyFor(sealer, context) {
64
+ if (!sealer.keyProvider) {
65
+ throw new StorageEncryptionKeyError(`${context.key} is encrypted, and this storage has no encryption key provider.`, { key: context.key });
66
+ }
67
+ let result;
68
+ try {
69
+ result = await sealer.keyProvider.getKey(context);
70
+ } catch (error) {
71
+ if (error instanceof StorageEncryptionError) throw error;
72
+ throw new StorageEncryptionKeyError(`The key provider failed for ${context.key}.`, { key: context.key, cause: error });
73
+ }
74
+ const pair = result && "key" in result && "id" in result ? result : { key: result };
75
+ const algorithm = pair.key?.algorithm;
76
+ if (!algorithm || algorithm.name !== "AES-GCM" || algorithm.length !== 256) {
77
+ throw new StorageEncryptionKeyError(`The key provider must return an AES-GCM 256-bit key for ${context.key}.`, { key: context.key });
78
+ }
79
+ if (!pair.key.usages.includes(context.operation)) {
80
+ throw new StorageEncryptionKeyError(`The key for ${context.key} does not allow ${context.operation}.`, { key: context.key });
81
+ }
82
+ return pair;
83
+ }
84
+ async function seal(sealer, context, storedKey, plaintext, meta) {
85
+ const { key, id } = await keyFor(sealer, { ...context, operation: "encrypt" });
86
+ const alg = sealer.provider.algorithm;
87
+ const out = await sealer.provider.encrypt(encoder.encode(plaintext), key, aad(storedKey, alg, id, meta));
88
+ const payload = { v: PAYLOAD_VERSION, alg, iv: toBase64(out.iv), data: toBase64(out.ciphertext), tag: toBase64(out.tag) };
89
+ if (id !== void 0) payload.kid = id;
90
+ return payload;
91
+ }
92
+ async function open(sealer, context, storedKey, payload, meta) {
93
+ const k = context.key;
94
+ const p = payload;
95
+ if (!p || typeof p !== "object" || typeof p.v !== "number") {
96
+ throw new StorageEncryptionMetadataError(`The encryption metadata of ${k} is missing its version.`, { key: k });
97
+ }
98
+ if (p.v !== PAYLOAD_VERSION) throw new StorageEncryptionVersionError(`${k} uses encryption format ${p.v}, which this release cannot read.`, { key: k });
99
+ if (typeof p.alg !== "string" || typeof p.iv !== "string" || typeof p.data !== "string" || typeof p.tag !== "string" || p.kid !== void 0 && typeof p.kid !== "string") {
100
+ throw new StorageEncryptionMetadataError(`The encryption metadata of ${k} is malformed.`, { key: k });
101
+ }
102
+ if (p.alg !== sealer.provider.algorithm) {
103
+ throw new StorageEncryptionAlgorithmError(`${k} is encrypted with ${p.alg}; the provider implements ${sealer.provider.algorithm}.`, { key: k });
104
+ }
105
+ let iv, ciphertext, tag;
106
+ try {
107
+ iv = fromBase64(p.iv);
108
+ ciphertext = fromBase64(p.data);
109
+ tag = fromBase64(p.tag);
110
+ } catch (error) {
111
+ throw new StorageEncryptedPayloadError(`The encrypted payload of ${k} is not valid base64.`, { key: k, cause: error });
112
+ }
113
+ if (iv.length !== 12 || tag.length !== 16) throw new StorageEncryptedPayloadError(`The encrypted payload of ${k} has a wrong IV or tag length.`, { key: k });
114
+ const { key } = await keyFor(sealer, { ...context, operation: "decrypt", keyId: p.kid });
115
+ let bytes;
116
+ try {
117
+ bytes = await sealer.provider.decrypt({ algorithm: p.alg, iv, ciphertext, tag }, key, aad(storedKey, p.alg, p.kid, meta));
118
+ } catch (error) {
119
+ throw new StorageDecryptionError(`${k} failed authentication: wrong key, or the stored entry was changed.`, { key: k, cause: error });
120
+ }
121
+ try {
122
+ return decoder.decode(bytes);
123
+ } catch (error) {
124
+ throw new StorageEncryptedPayloadError(`The decrypted value of ${k} is not UTF-8.`, { key: k, cause: error });
125
+ }
126
+ }
127
+ export {
128
+ PAYLOAD_VERSION,
129
+ open,
130
+ seal
131
+ };
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
+
16
+ // src/encryption/types.ts
17
+ var types_exports = {};
18
+ module.exports = __toCommonJS(types_exports);
@@ -0,0 +1,50 @@
1
+ /** Ciphertext and what decrypting it needs, apart from the key. */
2
+ export interface EncryptedData {
3
+ algorithm: string;
4
+ iv: Uint8Array;
5
+ ciphertext: Uint8Array;
6
+ /** The authentication tag. */
7
+ tag: Uint8Array;
8
+ }
9
+ /**
10
+ * The cryptographic backend. The storage only talks to this, so Web Crypto, Node's crypto or a
11
+ * WASM build can sit underneath without the storage knowing.
12
+ */
13
+ export interface CryptoProvider {
14
+ /** The algorithm it implements, as stored with each entry: `AES-256-GCM`. */
15
+ readonly algorithm: string;
16
+ /** Encrypts under a fresh random IV. `aad` is authenticated, not encrypted. */
17
+ encrypt(data: Uint8Array, key: CryptoKey, aad?: Uint8Array): Promise<EncryptedData>;
18
+ /** Rejects when authentication fails. */
19
+ decrypt(data: EncryptedData, key: CryptoKey, aad?: Uint8Array): Promise<Uint8Array>;
20
+ }
21
+ export interface EncryptionContext {
22
+ /** The entry's key, without the namespace. */
23
+ key: string;
24
+ namespace?: string;
25
+ operation: 'encrypt' | 'decrypt';
26
+ /** On decrypt, the id the key provider gave when the entry was written. */
27
+ keyId?: string;
28
+ }
29
+ /** A key, or a key with an id that is stored with the entry so rotation can find it again. */
30
+ export type EncryptionKey = CryptoKey | {
31
+ key: CryptoKey;
32
+ id: string;
33
+ };
34
+ export interface EncryptionKeyProvider {
35
+ getKey(context: EncryptionContext): Promise<EncryptionKey>;
36
+ }
37
+ /**
38
+ * Per-item encryption for a storage. It keeps a value confidential and detects tampering from
39
+ * anyone without the key; it does not replace authentication, authorization, HTTPS or secure
40
+ * cookies, and a key that lives next to the data protects nothing.
41
+ */
42
+ export interface EncryptionOptions {
43
+ keyProvider: EncryptionKeyProvider;
44
+ /**
45
+ * Defaults to Web Crypto (`globalThis.crypto.subtle`): the browser's, or Node's native one,
46
+ * global since Node 19. A WASM provider can go here for runtimes without either.
47
+ */
48
+ provider?: CryptoProvider;
49
+ }
50
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/encryption/types.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,UAAU,CAAC;IACf,UAAU,EAAE,UAAU,CAAC;IACvB,8BAA8B;IAC9B,GAAG,EAAE,UAAU,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACpF,yCAAyC;IACzC,OAAO,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACrF;AAED,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,SAAS,GAAG,SAAS,CAAC;IACjC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,8FAA8F;AAC9F,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG;IAAE,GAAG,EAAE,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAC5D;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,qBAAqB,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B"}
@@ -0,0 +1 @@
1
+ export {};
File without changes
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/encryption/web-crypto.ts
21
+ var web_crypto_exports = {};
22
+ __export(web_crypto_exports, {
23
+ webCryptoProvider: () => webCryptoProvider
24
+ });
25
+ module.exports = __toCommonJS(web_crypto_exports);
26
+
27
+ // src/core/errors.ts
28
+ var StorageError = class extends Error {
29
+ constructor(code, message, options = {}) {
30
+ super(message);
31
+ this.name = new.target.name;
32
+ this.code = code;
33
+ this.key = options.key;
34
+ if (options.cause !== void 0) this.cause = options.cause;
35
+ }
36
+ };
37
+ var StorageEncryptionError = class extends StorageError {
38
+ };
39
+ var StorageEncryptionUnavailableError = class extends StorageEncryptionError {
40
+ constructor(message, options = {}) {
41
+ super("encryption-unavailable", message, options);
42
+ }
43
+ };
44
+
45
+ // src/encryption/web-crypto.ts
46
+ var IV_BYTES = 12;
47
+ var TAG_BYTES = 16;
48
+ function webCryptoProvider(crypto) {
49
+ const resolve = () => {
50
+ const c = crypto ?? globalThis.crypto;
51
+ if (!c?.subtle || !c.getRandomValues) {
52
+ throw new StorageEncryptionUnavailableError("Web Crypto is not available in this runtime; pass a provider in the encryption options.");
53
+ }
54
+ return c;
55
+ };
56
+ return {
57
+ algorithm: "AES-256-GCM",
58
+ async encrypt(data, key, aad) {
59
+ const c = resolve();
60
+ const iv = c.getRandomValues(new Uint8Array(IV_BYTES));
61
+ const params = { name: "AES-GCM", iv, tagLength: TAG_BYTES * 8 };
62
+ if (aad) params.additionalData = aad;
63
+ const sealed = new Uint8Array(await c.subtle.encrypt(params, key, data));
64
+ return {
65
+ algorithm: "AES-256-GCM",
66
+ iv,
67
+ ciphertext: sealed.slice(0, sealed.length - TAG_BYTES),
68
+ tag: sealed.slice(sealed.length - TAG_BYTES)
69
+ };
70
+ },
71
+ async decrypt(data, key, aad) {
72
+ const c = resolve();
73
+ const sealed = new Uint8Array(data.ciphertext.length + data.tag.length);
74
+ sealed.set(data.ciphertext);
75
+ sealed.set(data.tag, data.ciphertext.length);
76
+ const params = { name: "AES-GCM", iv: data.iv, tagLength: TAG_BYTES * 8 };
77
+ if (aad) params.additionalData = aad;
78
+ return new Uint8Array(await c.subtle.decrypt(params, key, sealed));
79
+ }
80
+ };
81
+ }
@@ -0,0 +1,9 @@
1
+ import type { CryptoProvider } from './types.js';
2
+ /**
3
+ * AES-256-GCM over Web Crypto. Each call draws a new 96-bit random IV, so an IV is never
4
+ * deliberately reused; keep one key under about 2^32 writes before rotating it.
5
+ *
6
+ * `crypto` defaults to the global, read on first use so the module imports anywhere.
7
+ */
8
+ export declare function webCryptoProvider(crypto?: Crypto): CryptoProvider;
9
+ //# sourceMappingURL=web-crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-crypto.d.ts","sourceRoot":"","sources":["../../src/encryption/web-crypto.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAKjD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,cAAc,CAiCjE"}
@@ -0,0 +1,45 @@
1
+ import { StorageEncryptionUnavailableError } from '../core/errors.js';
2
+ const IV_BYTES = 12;
3
+ const TAG_BYTES = 16;
4
+ /**
5
+ * AES-256-GCM over Web Crypto. Each call draws a new 96-bit random IV, so an IV is never
6
+ * deliberately reused; keep one key under about 2^32 writes before rotating it.
7
+ *
8
+ * `crypto` defaults to the global, read on first use so the module imports anywhere.
9
+ */
10
+ export function webCryptoProvider(crypto) {
11
+ const resolve = () => {
12
+ const c = crypto ?? globalThis.crypto;
13
+ if (!c?.subtle || !c.getRandomValues) {
14
+ throw new StorageEncryptionUnavailableError('Web Crypto is not available in this runtime; pass a provider in the encryption options.');
15
+ }
16
+ return c;
17
+ };
18
+ return {
19
+ algorithm: 'AES-256-GCM',
20
+ async encrypt(data, key, aad) {
21
+ const c = resolve();
22
+ const iv = c.getRandomValues(new Uint8Array(IV_BYTES));
23
+ const params = { name: 'AES-GCM', iv, tagLength: TAG_BYTES * 8 };
24
+ if (aad)
25
+ params.additionalData = aad;
26
+ const sealed = new Uint8Array(await c.subtle.encrypt(params, key, data));
27
+ return {
28
+ algorithm: 'AES-256-GCM',
29
+ iv,
30
+ ciphertext: sealed.slice(0, sealed.length - TAG_BYTES),
31
+ tag: sealed.slice(sealed.length - TAG_BYTES),
32
+ };
33
+ },
34
+ async decrypt(data, key, aad) {
35
+ const c = resolve();
36
+ const sealed = new Uint8Array(data.ciphertext.length + data.tag.length);
37
+ sealed.set(data.ciphertext);
38
+ sealed.set(data.tag, data.ciphertext.length);
39
+ const params = { name: 'AES-GCM', iv: data.iv, tagLength: TAG_BYTES * 8 };
40
+ if (aad)
41
+ params.additionalData = aad;
42
+ return new Uint8Array(await c.subtle.decrypt(params, key, sealed));
43
+ },
44
+ };
45
+ }