@efsf/typescript 0.1.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.
- package/README.md +242 -0
- package/dist/index.d.mts +990 -0
- package/dist/index.d.ts +990 -0
- package/dist/index.js +1382 -0
- package/dist/index.mjs +1322 -0
- package/package.json +85 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1382 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AttestationAuthority: () => AttestationAuthority,
|
|
34
|
+
AttestationError: () => AttestationError,
|
|
35
|
+
BackendError: () => BackendError,
|
|
36
|
+
ChainOfCustody: () => ChainOfCustody,
|
|
37
|
+
CryptoError: () => CryptoError,
|
|
38
|
+
CryptoProvider: () => CryptoProvider,
|
|
39
|
+
DataClassification: () => DataClassification,
|
|
40
|
+
DataEncryptionKey: () => DataEncryptionKey,
|
|
41
|
+
DestructionCertificate: () => DestructionCertificate,
|
|
42
|
+
DestructionMethod: () => DestructionMethod,
|
|
43
|
+
EFSFError: () => EFSFError,
|
|
44
|
+
EncryptedPayload: () => EncryptedPayload,
|
|
45
|
+
EphemeralRecord: () => EphemeralRecord,
|
|
46
|
+
EphemeralStore: () => EphemeralStore,
|
|
47
|
+
MemoryBackend: () => MemoryBackend,
|
|
48
|
+
RecordExpiredError: () => RecordExpiredError,
|
|
49
|
+
RecordNotFoundError: () => RecordNotFoundError,
|
|
50
|
+
RedisBackend: () => RedisBackend,
|
|
51
|
+
ResourceInfo: () => ResourceInfo,
|
|
52
|
+
SealedContext: () => SealedContext,
|
|
53
|
+
SealedExecution: () => SealedExecution,
|
|
54
|
+
TTLViolationError: () => TTLViolationError,
|
|
55
|
+
VERSION: () => VERSION,
|
|
56
|
+
ValidationError: () => ValidationError,
|
|
57
|
+
constantTimeCompare: () => constantTimeCompare,
|
|
58
|
+
createBackend: () => createBackend,
|
|
59
|
+
getDefaultTTL: () => getDefaultTTL,
|
|
60
|
+
getMaxTTL: () => getMaxTTL,
|
|
61
|
+
parseTTL: () => parseTTL,
|
|
62
|
+
sealed: () => sealed,
|
|
63
|
+
secureZeroMemory: () => secureZeroMemory
|
|
64
|
+
});
|
|
65
|
+
module.exports = __toCommonJS(index_exports);
|
|
66
|
+
|
|
67
|
+
// src/certificate.ts
|
|
68
|
+
var crypto = __toESM(require("crypto"));
|
|
69
|
+
var import_uuid = require("uuid");
|
|
70
|
+
|
|
71
|
+
// src/exceptions.ts
|
|
72
|
+
var EFSFError = class extends Error {
|
|
73
|
+
constructor(message) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = "EFSFError";
|
|
76
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var RecordNotFoundError = class extends EFSFError {
|
|
80
|
+
constructor(recordId, message) {
|
|
81
|
+
super(message ?? `Record not found: ${recordId}`);
|
|
82
|
+
this.recordId = recordId;
|
|
83
|
+
this.name = "RecordNotFoundError";
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var RecordExpiredError = class extends EFSFError {
|
|
87
|
+
constructor(recordId, expiredAt) {
|
|
88
|
+
const msg = expiredAt ? `Record expired: ${recordId} (expired at ${expiredAt})` : `Record expired: ${recordId}`;
|
|
89
|
+
super(msg);
|
|
90
|
+
this.recordId = recordId;
|
|
91
|
+
this.expiredAt = expiredAt;
|
|
92
|
+
this.name = "RecordExpiredError";
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var CryptoError = class extends EFSFError {
|
|
96
|
+
constructor(operation, message) {
|
|
97
|
+
super(message ?? `Cryptographic operation failed: ${operation}`);
|
|
98
|
+
this.operation = operation;
|
|
99
|
+
this.name = "CryptoError";
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var AttestationError = class extends EFSFError {
|
|
103
|
+
constructor(message) {
|
|
104
|
+
super(message ?? "Attestation failed");
|
|
105
|
+
this.name = "AttestationError";
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var BackendError = class extends EFSFError {
|
|
109
|
+
constructor(backend, message) {
|
|
110
|
+
super(message ?? `Backend error: ${backend}`);
|
|
111
|
+
this.backend = backend;
|
|
112
|
+
this.name = "BackendError";
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var ValidationError = class extends EFSFError {
|
|
116
|
+
constructor(field, message) {
|
|
117
|
+
super(message ?? `Validation error: ${field}`);
|
|
118
|
+
this.field = field;
|
|
119
|
+
this.name = "ValidationError";
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var TTLViolationError = class extends EFSFError {
|
|
123
|
+
constructor(recordId, expectedTTL, actualTTL) {
|
|
124
|
+
const msg = actualTTL ? `TTL violation for record ${recordId}: expected ${expectedTTL}, got ${actualTTL}` : `TTL violation for record ${recordId}: expected ${expectedTTL}`;
|
|
125
|
+
super(msg);
|
|
126
|
+
this.recordId = recordId;
|
|
127
|
+
this.expectedTTL = expectedTTL;
|
|
128
|
+
this.actualTTL = actualTTL;
|
|
129
|
+
this.name = "TTLViolationError";
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// src/certificate.ts
|
|
134
|
+
var DestructionMethod = /* @__PURE__ */ ((DestructionMethod2) => {
|
|
135
|
+
DestructionMethod2["CRYPTO_SHRED"] = "crypto_shred";
|
|
136
|
+
DestructionMethod2["MEMORY_ZERO"] = "memory_zero";
|
|
137
|
+
DestructionMethod2["SECURE_DELETE"] = "secure_delete";
|
|
138
|
+
DestructionMethod2["TEE_EXIT"] = "tee_exit";
|
|
139
|
+
DestructionMethod2["TTL_EXPIRE"] = "ttl_expire";
|
|
140
|
+
DestructionMethod2["MANUAL"] = "manual";
|
|
141
|
+
return DestructionMethod2;
|
|
142
|
+
})(DestructionMethod || {});
|
|
143
|
+
var ResourceInfo = class {
|
|
144
|
+
constructor(resourceType, resourceId, classification, metadata = {}) {
|
|
145
|
+
this.resourceType = resourceType;
|
|
146
|
+
this.resourceId = resourceId;
|
|
147
|
+
this.classification = classification;
|
|
148
|
+
this.metadata = metadata;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Convert to a plain object for serialization.
|
|
152
|
+
*/
|
|
153
|
+
toDict() {
|
|
154
|
+
return {
|
|
155
|
+
type: this.resourceType,
|
|
156
|
+
id: this.resourceId,
|
|
157
|
+
classification: this.classification,
|
|
158
|
+
metadata: this.metadata
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var ChainOfCustody = class {
|
|
163
|
+
constructor(createdAt, createdBy = null) {
|
|
164
|
+
this.createdAt = createdAt;
|
|
165
|
+
this.createdBy = createdBy;
|
|
166
|
+
}
|
|
167
|
+
accessLog = [];
|
|
168
|
+
hashChain = [];
|
|
169
|
+
/**
|
|
170
|
+
* Record an access event and extend the hash chain.
|
|
171
|
+
*
|
|
172
|
+
* @param accessor - Identity of the accessor
|
|
173
|
+
* @param action - Action performed (create, read, destroy, etc.)
|
|
174
|
+
* @param timestamp - Optional timestamp (defaults to now)
|
|
175
|
+
*/
|
|
176
|
+
addAccess(accessor, action, timestamp) {
|
|
177
|
+
const ts = timestamp ?? /* @__PURE__ */ new Date();
|
|
178
|
+
const event = {
|
|
179
|
+
timestamp: ts.toISOString(),
|
|
180
|
+
accessor,
|
|
181
|
+
action
|
|
182
|
+
};
|
|
183
|
+
this.accessLog.push(event);
|
|
184
|
+
const eventHash = crypto.createHash("sha256").update(JSON.stringify(event)).digest("hex");
|
|
185
|
+
let chained;
|
|
186
|
+
if (this.hashChain.length > 0) {
|
|
187
|
+
chained = crypto.createHash("sha256").update(this.hashChain[this.hashChain.length - 1] + eventHash).digest("hex");
|
|
188
|
+
} else {
|
|
189
|
+
chained = eventHash;
|
|
190
|
+
}
|
|
191
|
+
this.hashChain.push(chained);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Convert to a plain object for serialization.
|
|
195
|
+
* Returns only the last 5 hashes for brevity.
|
|
196
|
+
*/
|
|
197
|
+
toDict() {
|
|
198
|
+
return {
|
|
199
|
+
created_at: this.createdAt.toISOString(),
|
|
200
|
+
created_by: this.createdBy,
|
|
201
|
+
access_count: this.accessLog.length,
|
|
202
|
+
hash_chain: this.hashChain.slice(-5)
|
|
203
|
+
// Last 5 hashes
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var DestructionCertificate = class _DestructionCertificate {
|
|
208
|
+
constructor(certificateId, version, resource, destructionMethod, destructionTimestamp, verifiedBy, chainOfCustody = null, signature = null) {
|
|
209
|
+
this.certificateId = certificateId;
|
|
210
|
+
this.version = version;
|
|
211
|
+
this.resource = resource;
|
|
212
|
+
this.destructionMethod = destructionMethod;
|
|
213
|
+
this.destructionTimestamp = destructionTimestamp;
|
|
214
|
+
this.verifiedBy = verifiedBy;
|
|
215
|
+
this.chainOfCustody = chainOfCustody;
|
|
216
|
+
this.signature = signature;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Factory method to create a new destruction certificate.
|
|
220
|
+
*
|
|
221
|
+
* @param resource - The destroyed resource info
|
|
222
|
+
* @param destructionMethod - How the data was destroyed
|
|
223
|
+
* @param verifiedBy - Authority that verified the destruction
|
|
224
|
+
* @param chainOfCustody - Optional chain of custody
|
|
225
|
+
*/
|
|
226
|
+
static create(resource, destructionMethod, verifiedBy = "efsf-local-authority", chainOfCustody = null) {
|
|
227
|
+
return new _DestructionCertificate(
|
|
228
|
+
(0, import_uuid.v4)(),
|
|
229
|
+
"1.0",
|
|
230
|
+
resource,
|
|
231
|
+
destructionMethod,
|
|
232
|
+
/* @__PURE__ */ new Date(),
|
|
233
|
+
verifiedBy,
|
|
234
|
+
chainOfCustody
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Convert to a plain object for serialization.
|
|
239
|
+
*
|
|
240
|
+
* @param includeSignature - Whether to include the signature field
|
|
241
|
+
*/
|
|
242
|
+
toDict(includeSignature = true) {
|
|
243
|
+
const data = {
|
|
244
|
+
version: this.version,
|
|
245
|
+
certificate_id: this.certificateId,
|
|
246
|
+
resource: this.resource.toDict(),
|
|
247
|
+
destruction: {
|
|
248
|
+
method: this.destructionMethod,
|
|
249
|
+
timestamp: this.destructionTimestamp.toISOString(),
|
|
250
|
+
verified_by: this.verifiedBy
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
if (this.chainOfCustody) {
|
|
254
|
+
data.chain_of_custody = this.chainOfCustody.toDict();
|
|
255
|
+
}
|
|
256
|
+
if (includeSignature && this.signature) {
|
|
257
|
+
data.signature = this.signature;
|
|
258
|
+
}
|
|
259
|
+
return data;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Convert to JSON string.
|
|
263
|
+
*/
|
|
264
|
+
toJSON(indent) {
|
|
265
|
+
return JSON.stringify(this.toDict(), null, indent);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get canonical byte representation for signing.
|
|
269
|
+
*
|
|
270
|
+
* Uses deterministic JSON serialization (sorted keys, no whitespace)
|
|
271
|
+
* to ensure consistent signatures.
|
|
272
|
+
*/
|
|
273
|
+
canonicalBytes() {
|
|
274
|
+
const data = this.toDict(false);
|
|
275
|
+
const sortedJson = JSON.stringify(data, Object.keys(data).sort());
|
|
276
|
+
return Buffer.from(sortedJson, "utf-8");
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Compute SHA-256 hash of the certificate.
|
|
280
|
+
*/
|
|
281
|
+
computeHash() {
|
|
282
|
+
return crypto.createHash("sha256").update(this.canonicalBytes()).digest("hex");
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
var AttestationAuthority = class {
|
|
286
|
+
/**
|
|
287
|
+
* Create a new AttestationAuthority.
|
|
288
|
+
*
|
|
289
|
+
* @param authorityId - Identifier for this authority
|
|
290
|
+
*/
|
|
291
|
+
constructor(authorityId = "efsf-local-authority") {
|
|
292
|
+
this.authorityId = authorityId;
|
|
293
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
|
|
294
|
+
this.privateKey = privateKey;
|
|
295
|
+
this.publicKey = publicKey;
|
|
296
|
+
}
|
|
297
|
+
privateKey;
|
|
298
|
+
publicKey;
|
|
299
|
+
issuedCertificates = /* @__PURE__ */ new Map();
|
|
300
|
+
/**
|
|
301
|
+
* Get the public key as raw bytes.
|
|
302
|
+
*/
|
|
303
|
+
get publicKeyBytes() {
|
|
304
|
+
return this.publicKey.export({ type: "spki", format: "der" });
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Get the public key as base64-encoded string.
|
|
308
|
+
*/
|
|
309
|
+
get publicKeyB64() {
|
|
310
|
+
return this.publicKeyBytes.toString("base64");
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Sign a destruction certificate.
|
|
314
|
+
*
|
|
315
|
+
* @param certificate - The certificate to sign
|
|
316
|
+
* @returns The signed certificate
|
|
317
|
+
*/
|
|
318
|
+
signCertificate(certificate) {
|
|
319
|
+
const message = certificate.canonicalBytes();
|
|
320
|
+
const signature = crypto.sign(null, message, this.privateKey);
|
|
321
|
+
certificate.signature = signature.toString("base64");
|
|
322
|
+
certificate.verifiedBy = this.authorityId;
|
|
323
|
+
this.issuedCertificates.set(certificate.certificateId, certificate);
|
|
324
|
+
return certificate;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Verify a certificate's signature.
|
|
328
|
+
*
|
|
329
|
+
* @param certificate - The certificate to verify
|
|
330
|
+
* @returns true if the signature is valid
|
|
331
|
+
* @throws AttestationError if verification fails
|
|
332
|
+
*/
|
|
333
|
+
verifyCertificate(certificate) {
|
|
334
|
+
if (!certificate.signature) {
|
|
335
|
+
throw new AttestationError("Certificate has no signature");
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const message = certificate.canonicalBytes();
|
|
339
|
+
const signature = Buffer.from(certificate.signature, "base64");
|
|
340
|
+
return crypto.verify(null, message, this.publicKey, signature);
|
|
341
|
+
} catch (e) {
|
|
342
|
+
throw new AttestationError(`Signature verification failed: ${e}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Issue a new signed destruction certificate.
|
|
347
|
+
*
|
|
348
|
+
* @param resourceType - Type of resource (ephemeral_data, sealed_compute, etc.)
|
|
349
|
+
* @param resourceId - Unique identifier of the resource
|
|
350
|
+
* @param classification - Data classification level
|
|
351
|
+
* @param destructionMethod - How the data was destroyed
|
|
352
|
+
* @param chainOfCustody - Optional chain of custody
|
|
353
|
+
* @param metadata - Optional additional metadata
|
|
354
|
+
* @returns The signed certificate
|
|
355
|
+
*/
|
|
356
|
+
issueCertificate(resourceType, resourceId, classification, destructionMethod, chainOfCustody = null, metadata = {}) {
|
|
357
|
+
const resource = new ResourceInfo(resourceType, resourceId, classification, metadata);
|
|
358
|
+
const certificate = DestructionCertificate.create(
|
|
359
|
+
resource,
|
|
360
|
+
destructionMethod,
|
|
361
|
+
this.authorityId,
|
|
362
|
+
chainOfCustody
|
|
363
|
+
);
|
|
364
|
+
return this.signCertificate(certificate);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Retrieve a previously issued certificate by ID.
|
|
368
|
+
*
|
|
369
|
+
* @param certificateId - The certificate ID
|
|
370
|
+
* @returns The certificate or null if not found
|
|
371
|
+
*/
|
|
372
|
+
getCertificate(certificateId) {
|
|
373
|
+
return this.issuedCertificates.get(certificateId) ?? null;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* List issued certificates with optional filtering.
|
|
377
|
+
*
|
|
378
|
+
* @param resourceId - Optional resource ID to filter by
|
|
379
|
+
* @param since - Optional date to filter certificates issued after
|
|
380
|
+
* @returns List of certificates sorted by destruction timestamp (newest first)
|
|
381
|
+
*/
|
|
382
|
+
listCertificates(resourceId, since) {
|
|
383
|
+
let certs = Array.from(this.issuedCertificates.values());
|
|
384
|
+
if (resourceId) {
|
|
385
|
+
certs = certs.filter((c) => c.resource.resourceId === resourceId);
|
|
386
|
+
}
|
|
387
|
+
if (since) {
|
|
388
|
+
certs = certs.filter((c) => c.destructionTimestamp >= since);
|
|
389
|
+
}
|
|
390
|
+
return certs.sort(
|
|
391
|
+
(a, b) => b.destructionTimestamp.getTime() - a.destructionTimestamp.getTime()
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// src/crypto.ts
|
|
397
|
+
var crypto2 = __toESM(require("crypto"));
|
|
398
|
+
var ALGORITHM = "aes-256-gcm";
|
|
399
|
+
var KEY_LENGTH = 32;
|
|
400
|
+
var NONCE_LENGTH = 12;
|
|
401
|
+
var TAG_LENGTH = 16;
|
|
402
|
+
var EncryptedPayload = class _EncryptedPayload {
|
|
403
|
+
constructor(ciphertext, nonce, keyId, algorithm = "AES-256-GCM") {
|
|
404
|
+
this.ciphertext = ciphertext;
|
|
405
|
+
this.nonce = nonce;
|
|
406
|
+
this.keyId = keyId;
|
|
407
|
+
this.algorithm = algorithm;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Convert to a plain object for serialization.
|
|
411
|
+
*/
|
|
412
|
+
toDict() {
|
|
413
|
+
return {
|
|
414
|
+
ciphertext: this.ciphertext,
|
|
415
|
+
nonce: this.nonce,
|
|
416
|
+
key_id: this.keyId,
|
|
417
|
+
algorithm: this.algorithm
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Reconstruct from serialized data.
|
|
422
|
+
*/
|
|
423
|
+
static fromDict(data) {
|
|
424
|
+
return new _EncryptedPayload(
|
|
425
|
+
data.ciphertext,
|
|
426
|
+
data.nonce,
|
|
427
|
+
data.key_id,
|
|
428
|
+
data.algorithm ?? "AES-256-GCM"
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
var DataEncryptionKey = class _DataEncryptionKey {
|
|
433
|
+
constructor(keyId, _keyMaterial, createdAt, expiresAt) {
|
|
434
|
+
this.keyId = keyId;
|
|
435
|
+
this._keyMaterial = _keyMaterial;
|
|
436
|
+
this.createdAt = createdAt;
|
|
437
|
+
this.expiresAt = expiresAt;
|
|
438
|
+
}
|
|
439
|
+
_destroyed = false;
|
|
440
|
+
_destroyedAt = null;
|
|
441
|
+
/**
|
|
442
|
+
* Generate a new DEK with the specified TTL.
|
|
443
|
+
*
|
|
444
|
+
* @param ttlMs - Time-to-live in milliseconds
|
|
445
|
+
* @param keyId - Optional key identifier (auto-generated if not provided)
|
|
446
|
+
*/
|
|
447
|
+
static generate(ttlMs, keyId) {
|
|
448
|
+
const now = /* @__PURE__ */ new Date();
|
|
449
|
+
return new _DataEncryptionKey(
|
|
450
|
+
keyId ?? crypto2.randomBytes(16).toString("hex"),
|
|
451
|
+
crypto2.randomBytes(KEY_LENGTH),
|
|
452
|
+
now,
|
|
453
|
+
new Date(now.getTime() + ttlMs)
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Get the key material for cryptographic operations.
|
|
458
|
+
* @throws CryptoError if the key has been destroyed
|
|
459
|
+
*/
|
|
460
|
+
get keyMaterial() {
|
|
461
|
+
if (this._destroyed) {
|
|
462
|
+
throw new CryptoError("access", "Key has been destroyed");
|
|
463
|
+
}
|
|
464
|
+
return this._keyMaterial;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Check if the key has been destroyed.
|
|
468
|
+
*/
|
|
469
|
+
get destroyed() {
|
|
470
|
+
return this._destroyed;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get the timestamp when the key was destroyed, if applicable.
|
|
474
|
+
*/
|
|
475
|
+
get destroyedAt() {
|
|
476
|
+
return this._destroyedAt;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Check if the key is expired or destroyed.
|
|
480
|
+
*/
|
|
481
|
+
get isExpired() {
|
|
482
|
+
return /* @__PURE__ */ new Date() >= this.expiresAt || this._destroyed;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Securely destroy the key material (crypto-shredding).
|
|
486
|
+
*
|
|
487
|
+
* This overwrites the key material with random data then zeros,
|
|
488
|
+
* making any data encrypted with this key permanently unrecoverable.
|
|
489
|
+
*
|
|
490
|
+
* Note: In JavaScript/Node.js, we cannot guarantee memory is fully
|
|
491
|
+
* zeroed due to garbage collection. This is best-effort. For true
|
|
492
|
+
* security guarantees, use hardware security modules (HSM).
|
|
493
|
+
*/
|
|
494
|
+
destroy() {
|
|
495
|
+
crypto2.randomFillSync(this._keyMaterial);
|
|
496
|
+
this._keyMaterial.fill(0);
|
|
497
|
+
this._destroyed = true;
|
|
498
|
+
this._destroyedAt = /* @__PURE__ */ new Date();
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
var CryptoProvider = class {
|
|
502
|
+
masterKey;
|
|
503
|
+
keys = /* @__PURE__ */ new Map();
|
|
504
|
+
/**
|
|
505
|
+
* Create a new CryptoProvider.
|
|
506
|
+
*
|
|
507
|
+
* @param masterKey - Optional master key for key derivation.
|
|
508
|
+
* If not provided, a random key is generated.
|
|
509
|
+
* In production, this should come from a KMS.
|
|
510
|
+
*/
|
|
511
|
+
constructor(masterKey) {
|
|
512
|
+
this.masterKey = masterKey ?? crypto2.randomBytes(KEY_LENGTH);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Generate a new Data Encryption Key with the specified TTL.
|
|
516
|
+
*
|
|
517
|
+
* @param ttlMs - Time-to-live in milliseconds
|
|
518
|
+
* @returns The generated DEK
|
|
519
|
+
*/
|
|
520
|
+
generateDEK(ttlMs) {
|
|
521
|
+
const dek = DataEncryptionKey.generate(ttlMs);
|
|
522
|
+
this.keys.set(dek.keyId, dek);
|
|
523
|
+
return dek;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Retrieve a DEK by its identifier.
|
|
527
|
+
*
|
|
528
|
+
* @param keyId - The key identifier
|
|
529
|
+
* @returns The DEK if found and valid, null otherwise
|
|
530
|
+
*/
|
|
531
|
+
getDEK(keyId) {
|
|
532
|
+
const dek = this.keys.get(keyId);
|
|
533
|
+
if (!dek || dek.destroyed || dek.isExpired) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
return dek;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Destroy a DEK (crypto-shredding).
|
|
540
|
+
*
|
|
541
|
+
* After destruction, any data encrypted with this key becomes
|
|
542
|
+
* permanently unrecoverable.
|
|
543
|
+
*
|
|
544
|
+
* @param keyId - The key identifier
|
|
545
|
+
* @returns true if the key was found and destroyed, false otherwise
|
|
546
|
+
*/
|
|
547
|
+
destroyDEK(keyId) {
|
|
548
|
+
const dek = this.keys.get(keyId);
|
|
549
|
+
if (dek) {
|
|
550
|
+
dek.destroy();
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Encrypt plaintext using AES-256-GCM.
|
|
557
|
+
*
|
|
558
|
+
* @param plaintext - The data to encrypt
|
|
559
|
+
* @param dek - The Data Encryption Key to use
|
|
560
|
+
* @param associatedData - Optional additional authenticated data (AAD)
|
|
561
|
+
* @returns The encrypted payload
|
|
562
|
+
* @throws CryptoError if the key is destroyed or expired
|
|
563
|
+
*/
|
|
564
|
+
encrypt(plaintext, dek, associatedData) {
|
|
565
|
+
if (dek.destroyed || dek.isExpired) {
|
|
566
|
+
throw new CryptoError("encrypt", "Key is destroyed or expired");
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
const nonce = crypto2.randomBytes(NONCE_LENGTH);
|
|
570
|
+
const cipher = crypto2.createCipheriv(ALGORITHM, dek.keyMaterial, nonce, {
|
|
571
|
+
authTagLength: TAG_LENGTH
|
|
572
|
+
});
|
|
573
|
+
if (associatedData) {
|
|
574
|
+
cipher.setAAD(associatedData);
|
|
575
|
+
}
|
|
576
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
577
|
+
const authTag = cipher.getAuthTag();
|
|
578
|
+
const combined = Buffer.concat([encrypted, authTag]);
|
|
579
|
+
return new EncryptedPayload(
|
|
580
|
+
combined.toString("base64"),
|
|
581
|
+
nonce.toString("base64"),
|
|
582
|
+
dek.keyId
|
|
583
|
+
);
|
|
584
|
+
} catch (e) {
|
|
585
|
+
throw new CryptoError("encrypt", String(e));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Decrypt an encrypted payload.
|
|
590
|
+
*
|
|
591
|
+
* @param payload - The encrypted payload to decrypt
|
|
592
|
+
* @param associatedData - Optional additional authenticated data (must match encryption)
|
|
593
|
+
* @returns The decrypted plaintext
|
|
594
|
+
* @throws CryptoError if decryption fails or key is unavailable
|
|
595
|
+
*/
|
|
596
|
+
decrypt(payload, associatedData) {
|
|
597
|
+
const dek = this.getDEK(payload.keyId);
|
|
598
|
+
if (!dek) {
|
|
599
|
+
throw new CryptoError(
|
|
600
|
+
"decrypt",
|
|
601
|
+
`Key ${payload.keyId} not found or destroyed (data is unrecoverable)`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
const combined = Buffer.from(payload.ciphertext, "base64");
|
|
606
|
+
const nonce = Buffer.from(payload.nonce, "base64");
|
|
607
|
+
const ciphertext = combined.subarray(0, combined.length - TAG_LENGTH);
|
|
608
|
+
const authTag = combined.subarray(combined.length - TAG_LENGTH);
|
|
609
|
+
const decipher = crypto2.createDecipheriv(ALGORITHM, dek.keyMaterial, nonce, {
|
|
610
|
+
authTagLength: TAG_LENGTH
|
|
611
|
+
});
|
|
612
|
+
decipher.setAuthTag(authTag);
|
|
613
|
+
if (associatedData) {
|
|
614
|
+
decipher.setAAD(associatedData);
|
|
615
|
+
}
|
|
616
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
617
|
+
} catch (e) {
|
|
618
|
+
throw new CryptoError("decrypt", String(e));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Encrypt a JSON-serializable object.
|
|
623
|
+
*
|
|
624
|
+
* @param data - The object to encrypt
|
|
625
|
+
* @param dek - The Data Encryption Key to use
|
|
626
|
+
* @returns The encrypted payload
|
|
627
|
+
*/
|
|
628
|
+
encryptJSON(data, dek) {
|
|
629
|
+
const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
|
|
630
|
+
return this.encrypt(plaintext, dek);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Decrypt a payload and parse as JSON.
|
|
634
|
+
*
|
|
635
|
+
* @param payload - The encrypted payload
|
|
636
|
+
* @returns The decrypted object
|
|
637
|
+
*/
|
|
638
|
+
decryptJSON(payload) {
|
|
639
|
+
const plaintext = this.decrypt(payload);
|
|
640
|
+
return JSON.parse(plaintext.toString("utf-8"));
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Derive a key from the master key using HKDF.
|
|
644
|
+
*
|
|
645
|
+
* @param context - Context bytes for domain separation
|
|
646
|
+
* @param length - Desired key length in bytes (default: 32)
|
|
647
|
+
* @returns The derived key material
|
|
648
|
+
*/
|
|
649
|
+
deriveKey(context, length = KEY_LENGTH) {
|
|
650
|
+
return Buffer.from(
|
|
651
|
+
crypto2.hkdfSync("sha256", this.masterKey, Buffer.alloc(0), context, length)
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
function constantTimeCompare(a, b) {
|
|
656
|
+
if (a.length !== b.length) {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
return crypto2.timingSafeEqual(a, b);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/record.ts
|
|
663
|
+
var import_uuid2 = require("uuid");
|
|
664
|
+
var DataClassification = /* @__PURE__ */ ((DataClassification2) => {
|
|
665
|
+
DataClassification2["TRANSIENT"] = "TRANSIENT";
|
|
666
|
+
DataClassification2["SHORT_LIVED"] = "SHORT_LIVED";
|
|
667
|
+
DataClassification2["RETENTION_BOUND"] = "RETENTION_BOUND";
|
|
668
|
+
DataClassification2["PERSISTENT"] = "PERSISTENT";
|
|
669
|
+
return DataClassification2;
|
|
670
|
+
})(DataClassification || {});
|
|
671
|
+
var CLASSIFICATION_DEFAULTS = {
|
|
672
|
+
["TRANSIENT" /* TRANSIENT */]: 60 * 60 * 1e3,
|
|
673
|
+
// 1 hour
|
|
674
|
+
["SHORT_LIVED" /* SHORT_LIVED */]: 24 * 60 * 60 * 1e3,
|
|
675
|
+
// 1 day
|
|
676
|
+
["RETENTION_BOUND" /* RETENTION_BOUND */]: 90 * 24 * 60 * 60 * 1e3,
|
|
677
|
+
// 90 days
|
|
678
|
+
["PERSISTENT" /* PERSISTENT */]: null
|
|
679
|
+
};
|
|
680
|
+
var CLASSIFICATION_MAX = {
|
|
681
|
+
["TRANSIENT" /* TRANSIENT */]: 24 * 60 * 60 * 1e3,
|
|
682
|
+
// 24 hours
|
|
683
|
+
["SHORT_LIVED" /* SHORT_LIVED */]: 7 * 24 * 60 * 60 * 1e3,
|
|
684
|
+
// 7 days
|
|
685
|
+
["RETENTION_BOUND" /* RETENTION_BOUND */]: 7 * 365 * 24 * 60 * 60 * 1e3,
|
|
686
|
+
// 7 years
|
|
687
|
+
["PERSISTENT" /* PERSISTENT */]: null
|
|
688
|
+
};
|
|
689
|
+
function getDefaultTTL(classification) {
|
|
690
|
+
return CLASSIFICATION_DEFAULTS[classification];
|
|
691
|
+
}
|
|
692
|
+
function getMaxTTL(classification) {
|
|
693
|
+
return CLASSIFICATION_MAX[classification];
|
|
694
|
+
}
|
|
695
|
+
var EphemeralRecord = class _EphemeralRecord {
|
|
696
|
+
constructor(id, classification, createdAt, expiresAt, ttl, encrypted = true, keyId = null, accessCount = 0, metadata = {}) {
|
|
697
|
+
this.id = id;
|
|
698
|
+
this.classification = classification;
|
|
699
|
+
this.createdAt = createdAt;
|
|
700
|
+
this.expiresAt = expiresAt;
|
|
701
|
+
this.ttl = ttl;
|
|
702
|
+
this.encrypted = encrypted;
|
|
703
|
+
this.keyId = keyId;
|
|
704
|
+
this.accessCount = accessCount;
|
|
705
|
+
this.metadata = metadata;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Factory method to create a new ephemeral record.
|
|
709
|
+
*
|
|
710
|
+
* @param options - Configuration options for the record
|
|
711
|
+
* @returns A new EphemeralRecord instance
|
|
712
|
+
* @throws Error if TTL exceeds the classification's maximum
|
|
713
|
+
*/
|
|
714
|
+
static create(options = {}) {
|
|
715
|
+
const classification = options.classification ?? "TRANSIENT" /* TRANSIENT */;
|
|
716
|
+
const now = /* @__PURE__ */ new Date();
|
|
717
|
+
let effectiveTTL = options.ttl ?? getDefaultTTL(classification);
|
|
718
|
+
if (effectiveTTL === null) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
`TTL required for ${classification} classification (no default available)`
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
const maxTTL = getMaxTTL(classification);
|
|
724
|
+
if (maxTTL !== null && effectiveTTL > maxTTL) {
|
|
725
|
+
throw new Error(
|
|
726
|
+
`TTL ${effectiveTTL}ms exceeds maximum ${maxTTL}ms for ${classification} classification`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
const expiresAt = new Date(now.getTime() + effectiveTTL);
|
|
730
|
+
return new _EphemeralRecord(
|
|
731
|
+
(0, import_uuid2.v4)(),
|
|
732
|
+
classification,
|
|
733
|
+
now,
|
|
734
|
+
expiresAt,
|
|
735
|
+
effectiveTTL,
|
|
736
|
+
true,
|
|
737
|
+
(0, import_uuid2.v4)(),
|
|
738
|
+
// Generate key ID for crypto-shredding
|
|
739
|
+
0,
|
|
740
|
+
options.metadata ?? {}
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Check if the record has expired.
|
|
745
|
+
*/
|
|
746
|
+
get isExpired() {
|
|
747
|
+
return /* @__PURE__ */ new Date() >= this.expiresAt;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Get remaining time until expiration in milliseconds.
|
|
751
|
+
*/
|
|
752
|
+
get timeRemaining() {
|
|
753
|
+
const remaining = this.expiresAt.getTime() - Date.now();
|
|
754
|
+
return Math.max(remaining, 0);
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Convert to a plain object for serialization.
|
|
758
|
+
*/
|
|
759
|
+
toDict() {
|
|
760
|
+
return {
|
|
761
|
+
id: this.id,
|
|
762
|
+
classification: this.classification,
|
|
763
|
+
created_at: this.createdAt.toISOString(),
|
|
764
|
+
expires_at: this.expiresAt.toISOString(),
|
|
765
|
+
ttl_seconds: this.ttl / 1e3,
|
|
766
|
+
encrypted: this.encrypted,
|
|
767
|
+
key_id: this.keyId,
|
|
768
|
+
access_count: this.accessCount,
|
|
769
|
+
metadata: this.metadata
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Reconstruct an EphemeralRecord from serialized data.
|
|
774
|
+
*/
|
|
775
|
+
static fromDict(data) {
|
|
776
|
+
return new _EphemeralRecord(
|
|
777
|
+
data.id,
|
|
778
|
+
data.classification,
|
|
779
|
+
new Date(data.created_at),
|
|
780
|
+
new Date(data.expires_at),
|
|
781
|
+
data.ttl_seconds * 1e3,
|
|
782
|
+
data.encrypted ?? true,
|
|
783
|
+
data.key_id ?? null,
|
|
784
|
+
data.access_count ?? 0,
|
|
785
|
+
data.metadata ?? {}
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
function parseTTL(ttlString) {
|
|
790
|
+
const pattern = /^(\d+)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?|d|days?)$/i;
|
|
791
|
+
const match = ttlString.trim().match(pattern);
|
|
792
|
+
if (!match) {
|
|
793
|
+
throw new Error(`Cannot parse TTL string: ${ttlString}`);
|
|
794
|
+
}
|
|
795
|
+
const value = parseInt(match[1], 10);
|
|
796
|
+
const unit = match[2].toLowerCase();
|
|
797
|
+
if (unit.startsWith("s")) {
|
|
798
|
+
return value * 1e3;
|
|
799
|
+
} else if (unit.startsWith("m")) {
|
|
800
|
+
return value * 60 * 1e3;
|
|
801
|
+
} else if (unit.startsWith("h")) {
|
|
802
|
+
return value * 60 * 60 * 1e3;
|
|
803
|
+
} else if (unit.startsWith("d")) {
|
|
804
|
+
return value * 24 * 60 * 60 * 1e3;
|
|
805
|
+
}
|
|
806
|
+
throw new Error(`Unknown time unit: ${unit}`);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/store.ts
|
|
810
|
+
var MemoryBackend = class {
|
|
811
|
+
data = /* @__PURE__ */ new Map();
|
|
812
|
+
async set(key, value, ttlSeconds) {
|
|
813
|
+
this.data.set(key, {
|
|
814
|
+
value,
|
|
815
|
+
expiresAt: Date.now() + ttlSeconds * 1e3
|
|
816
|
+
});
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
async get(key) {
|
|
820
|
+
const entry = this.data.get(key);
|
|
821
|
+
if (!entry) {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
if (Date.now() >= entry.expiresAt) {
|
|
825
|
+
this.data.delete(key);
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
return entry.value;
|
|
829
|
+
}
|
|
830
|
+
async delete(key) {
|
|
831
|
+
return this.data.delete(key);
|
|
832
|
+
}
|
|
833
|
+
async exists(key) {
|
|
834
|
+
const value = await this.get(key);
|
|
835
|
+
return value !== null;
|
|
836
|
+
}
|
|
837
|
+
async ttl(key) {
|
|
838
|
+
const entry = this.data.get(key);
|
|
839
|
+
if (!entry) {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
const remaining = Math.floor((entry.expiresAt - Date.now()) / 1e3);
|
|
843
|
+
return Math.max(0, remaining);
|
|
844
|
+
}
|
|
845
|
+
async close() {
|
|
846
|
+
this.data.clear();
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
var RedisBackend = class {
|
|
850
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
851
|
+
client;
|
|
852
|
+
prefix = "efsf:";
|
|
853
|
+
constructor(url, options) {
|
|
854
|
+
try {
|
|
855
|
+
const Redis = require("ioredis");
|
|
856
|
+
this.client = new Redis(url, options);
|
|
857
|
+
} catch {
|
|
858
|
+
throw new BackendError(
|
|
859
|
+
"redis",
|
|
860
|
+
"ioredis package not installed. Install with: npm install ioredis"
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
key(k) {
|
|
865
|
+
return `${this.prefix}${k}`;
|
|
866
|
+
}
|
|
867
|
+
async set(key, value, ttlSeconds) {
|
|
868
|
+
try {
|
|
869
|
+
await this.client.setex(this.key(key), ttlSeconds, value);
|
|
870
|
+
return true;
|
|
871
|
+
} catch (e) {
|
|
872
|
+
throw new BackendError("redis", `Failed to set: ${e}`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async get(key) {
|
|
876
|
+
try {
|
|
877
|
+
return await this.client.get(this.key(key));
|
|
878
|
+
} catch (e) {
|
|
879
|
+
throw new BackendError("redis", `Failed to get: ${e}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async delete(key) {
|
|
883
|
+
try {
|
|
884
|
+
const result = await this.client.del(this.key(key));
|
|
885
|
+
return result > 0;
|
|
886
|
+
} catch (e) {
|
|
887
|
+
throw new BackendError("redis", `Failed to delete: ${e}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
async exists(key) {
|
|
891
|
+
try {
|
|
892
|
+
const result = await this.client.exists(this.key(key));
|
|
893
|
+
return result > 0;
|
|
894
|
+
} catch (e) {
|
|
895
|
+
throw new BackendError("redis", `Failed to check exists: ${e}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
async ttl(key) {
|
|
899
|
+
try {
|
|
900
|
+
const result = await this.client.ttl(this.key(key));
|
|
901
|
+
return result > 0 ? result : null;
|
|
902
|
+
} catch (e) {
|
|
903
|
+
throw new BackendError("redis", `Failed to get TTL: ${e}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async close() {
|
|
907
|
+
await this.client.quit();
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
function createBackend(backendUrl) {
|
|
911
|
+
if (backendUrl === "memory://" || backendUrl === "memory") {
|
|
912
|
+
return new MemoryBackend();
|
|
913
|
+
}
|
|
914
|
+
const url = new URL(backendUrl);
|
|
915
|
+
if (url.protocol === "redis:" || url.protocol === "rediss:") {
|
|
916
|
+
return new RedisBackend(backendUrl);
|
|
917
|
+
}
|
|
918
|
+
throw new ValidationError("backend", `Unsupported backend scheme: ${url.protocol}`);
|
|
919
|
+
}
|
|
920
|
+
var EphemeralStore = class {
|
|
921
|
+
backend;
|
|
922
|
+
defaultTTL;
|
|
923
|
+
// ms
|
|
924
|
+
crypto;
|
|
925
|
+
attestationEnabled;
|
|
926
|
+
authority;
|
|
927
|
+
records = /* @__PURE__ */ new Map();
|
|
928
|
+
custody = /* @__PURE__ */ new Map();
|
|
929
|
+
certificates = /* @__PURE__ */ new Map();
|
|
930
|
+
constructor(options = {}) {
|
|
931
|
+
if (typeof options.backend === "string") {
|
|
932
|
+
this.backend = createBackend(options.backend);
|
|
933
|
+
} else if (options.backend) {
|
|
934
|
+
this.backend = options.backend;
|
|
935
|
+
} else {
|
|
936
|
+
this.backend = new MemoryBackend();
|
|
937
|
+
}
|
|
938
|
+
if (typeof options.defaultTTL === "string") {
|
|
939
|
+
this.defaultTTL = parseTTL(options.defaultTTL);
|
|
940
|
+
} else if (typeof options.defaultTTL === "number") {
|
|
941
|
+
this.defaultTTL = options.defaultTTL;
|
|
942
|
+
} else {
|
|
943
|
+
this.defaultTTL = 60 * 60 * 1e3;
|
|
944
|
+
}
|
|
945
|
+
this.crypto = options.cryptoProvider ?? new CryptoProvider();
|
|
946
|
+
this.attestationEnabled = options.attestation ?? true;
|
|
947
|
+
if (this.attestationEnabled) {
|
|
948
|
+
this.authority = options.attestationAuthority ?? new AttestationAuthority();
|
|
949
|
+
} else {
|
|
950
|
+
this.authority = null;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Store data with automatic encryption and TTL.
|
|
955
|
+
*
|
|
956
|
+
* @param data - Data to store (must be JSON-serializable)
|
|
957
|
+
* @param options - Storage options (TTL, classification, metadata)
|
|
958
|
+
* @returns The ephemeral record metadata
|
|
959
|
+
*/
|
|
960
|
+
async put(data, options = {}) {
|
|
961
|
+
let classification;
|
|
962
|
+
if (typeof options.classification === "string") {
|
|
963
|
+
classification = options.classification;
|
|
964
|
+
} else {
|
|
965
|
+
classification = options.classification ?? "TRANSIENT" /* TRANSIENT */;
|
|
966
|
+
}
|
|
967
|
+
let effectiveTTL;
|
|
968
|
+
if (options.ttl === void 0) {
|
|
969
|
+
effectiveTTL = this.defaultTTL;
|
|
970
|
+
} else if (typeof options.ttl === "string") {
|
|
971
|
+
effectiveTTL = parseTTL(options.ttl);
|
|
972
|
+
} else {
|
|
973
|
+
effectiveTTL = options.ttl;
|
|
974
|
+
}
|
|
975
|
+
const record = EphemeralRecord.create({
|
|
976
|
+
classification,
|
|
977
|
+
ttl: effectiveTTL,
|
|
978
|
+
metadata: options.metadata
|
|
979
|
+
});
|
|
980
|
+
const dek = this.crypto.generateDEK(effectiveTTL);
|
|
981
|
+
record.keyId = dek.keyId;
|
|
982
|
+
const encrypted = this.crypto.encryptJSON(data, dek);
|
|
983
|
+
const storagePayload = {
|
|
984
|
+
record: record.toDict(),
|
|
985
|
+
encrypted: encrypted.toDict()
|
|
986
|
+
};
|
|
987
|
+
const ttlSeconds = Math.ceil(effectiveTTL / 1e3);
|
|
988
|
+
await this.backend.set(record.id, JSON.stringify(storagePayload), ttlSeconds);
|
|
989
|
+
this.records.set(record.id, record);
|
|
990
|
+
if (this.attestationEnabled) {
|
|
991
|
+
const custody = new ChainOfCustody(record.createdAt, "ephemeral_store");
|
|
992
|
+
custody.addAccess("ephemeral_store", "create");
|
|
993
|
+
this.custody.set(record.id, custody);
|
|
994
|
+
}
|
|
995
|
+
return record;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Retrieve data by record ID.
|
|
999
|
+
*
|
|
1000
|
+
* @param recordId - The record identifier
|
|
1001
|
+
* @returns The decrypted data
|
|
1002
|
+
* @throws RecordNotFoundError if the record doesn't exist
|
|
1003
|
+
* @throws RecordExpiredError if the record has expired
|
|
1004
|
+
*/
|
|
1005
|
+
async get(recordId) {
|
|
1006
|
+
const raw = await this.backend.get(recordId);
|
|
1007
|
+
if (raw === null) {
|
|
1008
|
+
if (this.certificates.has(recordId)) {
|
|
1009
|
+
throw new RecordExpiredError(recordId);
|
|
1010
|
+
}
|
|
1011
|
+
throw new RecordNotFoundError(recordId);
|
|
1012
|
+
}
|
|
1013
|
+
const storagePayload = JSON.parse(raw);
|
|
1014
|
+
const record = EphemeralRecord.fromDict(storagePayload.record);
|
|
1015
|
+
const encrypted = EncryptedPayload.fromDict(storagePayload.encrypted);
|
|
1016
|
+
if (record.isExpired) {
|
|
1017
|
+
await this.handleExpiration(recordId);
|
|
1018
|
+
throw new RecordExpiredError(recordId, record.expiresAt.toISOString());
|
|
1019
|
+
}
|
|
1020
|
+
const data = this.crypto.decryptJSON(encrypted);
|
|
1021
|
+
const trackedRecord = this.records.get(recordId);
|
|
1022
|
+
if (trackedRecord) {
|
|
1023
|
+
trackedRecord.accessCount++;
|
|
1024
|
+
}
|
|
1025
|
+
const custody = this.custody.get(recordId);
|
|
1026
|
+
if (custody) {
|
|
1027
|
+
custody.addAccess("ephemeral_store", "read");
|
|
1028
|
+
}
|
|
1029
|
+
return data;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Check if a record exists and is not expired.
|
|
1033
|
+
*
|
|
1034
|
+
* @param recordId - The record identifier
|
|
1035
|
+
*/
|
|
1036
|
+
async exists(recordId) {
|
|
1037
|
+
return this.backend.exists(recordId);
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Get remaining TTL for a record in milliseconds.
|
|
1041
|
+
*
|
|
1042
|
+
* @param recordId - The record identifier
|
|
1043
|
+
* @returns Remaining TTL in milliseconds, or null if not found
|
|
1044
|
+
*/
|
|
1045
|
+
async ttl(recordId) {
|
|
1046
|
+
const seconds = await this.backend.ttl(recordId);
|
|
1047
|
+
if (seconds === null) {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
return seconds * 1e3;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Manually destroy a record immediately (crypto-shredding).
|
|
1054
|
+
*
|
|
1055
|
+
* This destroys the encryption key, making the data permanently
|
|
1056
|
+
* unrecoverable, and generates a destruction certificate.
|
|
1057
|
+
*
|
|
1058
|
+
* @param recordId - The record identifier
|
|
1059
|
+
* @returns The destruction certificate, or null if attestation is disabled
|
|
1060
|
+
*/
|
|
1061
|
+
async destroy(recordId) {
|
|
1062
|
+
return this.handleExpiration(recordId, "manual" /* MANUAL */);
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Handle record expiration or destruction.
|
|
1066
|
+
*/
|
|
1067
|
+
async handleExpiration(recordId, method = "crypto_shred" /* CRYPTO_SHRED */) {
|
|
1068
|
+
const record = this.records.get(recordId);
|
|
1069
|
+
const custody = this.custody.get(recordId);
|
|
1070
|
+
await this.backend.delete(recordId);
|
|
1071
|
+
if (record?.keyId) {
|
|
1072
|
+
this.crypto.destroyDEK(record.keyId);
|
|
1073
|
+
}
|
|
1074
|
+
let certificate = null;
|
|
1075
|
+
if (this.attestationEnabled && this.authority && record) {
|
|
1076
|
+
if (custody) {
|
|
1077
|
+
custody.addAccess("ephemeral_store", "destroy");
|
|
1078
|
+
}
|
|
1079
|
+
certificate = this.authority.issueCertificate(
|
|
1080
|
+
"ephemeral_data",
|
|
1081
|
+
recordId,
|
|
1082
|
+
record.classification,
|
|
1083
|
+
method,
|
|
1084
|
+
custody ?? null,
|
|
1085
|
+
{
|
|
1086
|
+
ttl_seconds: record.ttl / 1e3,
|
|
1087
|
+
access_count: record.accessCount,
|
|
1088
|
+
...record.metadata
|
|
1089
|
+
}
|
|
1090
|
+
);
|
|
1091
|
+
this.certificates.set(recordId, certificate);
|
|
1092
|
+
}
|
|
1093
|
+
this.records.delete(recordId);
|
|
1094
|
+
this.custody.delete(recordId);
|
|
1095
|
+
return certificate;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Get the destruction certificate for a destroyed record.
|
|
1099
|
+
*
|
|
1100
|
+
* @param recordId - The record identifier
|
|
1101
|
+
* @returns The certificate or null if not found
|
|
1102
|
+
*/
|
|
1103
|
+
getDestructionCertificate(recordId) {
|
|
1104
|
+
return this.certificates.get(recordId) ?? null;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* List all destruction certificates.
|
|
1108
|
+
*
|
|
1109
|
+
* @param since - Optional date to filter certificates issued after
|
|
1110
|
+
* @returns List of certificates sorted by destruction timestamp (newest first)
|
|
1111
|
+
*/
|
|
1112
|
+
listCertificates(since) {
|
|
1113
|
+
let certs = Array.from(this.certificates.values());
|
|
1114
|
+
if (since) {
|
|
1115
|
+
certs = certs.filter((c) => c.destructionTimestamp >= since);
|
|
1116
|
+
}
|
|
1117
|
+
return certs.sort(
|
|
1118
|
+
(a, b) => b.destructionTimestamp.getTime() - a.destructionTimestamp.getTime()
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Get store statistics.
|
|
1123
|
+
*/
|
|
1124
|
+
stats() {
|
|
1125
|
+
return {
|
|
1126
|
+
activeRecords: this.records.size,
|
|
1127
|
+
certificatesIssued: this.certificates.size,
|
|
1128
|
+
attestationEnabled: this.attestationEnabled
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Close the store and release resources.
|
|
1133
|
+
*/
|
|
1134
|
+
async close() {
|
|
1135
|
+
await this.backend.close();
|
|
1136
|
+
}
|
|
1137
|
+
// ============================================================
|
|
1138
|
+
// Internal accessors for testing
|
|
1139
|
+
// ============================================================
|
|
1140
|
+
/** @internal */
|
|
1141
|
+
get _crypto() {
|
|
1142
|
+
return this.crypto;
|
|
1143
|
+
}
|
|
1144
|
+
/** @internal */
|
|
1145
|
+
get _authority() {
|
|
1146
|
+
return this.authority;
|
|
1147
|
+
}
|
|
1148
|
+
/** @internal */
|
|
1149
|
+
get _records() {
|
|
1150
|
+
return this.records;
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
// src/sealed.ts
|
|
1155
|
+
var import_uuid3 = require("uuid");
|
|
1156
|
+
function secureZeroMemory(data) {
|
|
1157
|
+
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
|
|
1158
|
+
data.fill(0);
|
|
1159
|
+
} else if (Array.isArray(data)) {
|
|
1160
|
+
for (let i = 0; i < data.length; i++) {
|
|
1161
|
+
secureZeroMemory(data[i]);
|
|
1162
|
+
}
|
|
1163
|
+
data.length = 0;
|
|
1164
|
+
} else if (data && typeof data === "object") {
|
|
1165
|
+
for (const key of Object.keys(data)) {
|
|
1166
|
+
secureZeroMemory(data[key]);
|
|
1167
|
+
delete data[key];
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
var SealedContext = class {
|
|
1172
|
+
executionId;
|
|
1173
|
+
startedAt;
|
|
1174
|
+
sensitiveRefs = [];
|
|
1175
|
+
cleanupCallbacks = [];
|
|
1176
|
+
constructor(options = {}) {
|
|
1177
|
+
this.executionId = options.executionId ?? (0, import_uuid3.v4)();
|
|
1178
|
+
this.startedAt = /* @__PURE__ */ new Date();
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Track an object for cleanup on context exit.
|
|
1182
|
+
*
|
|
1183
|
+
* @param obj - Object to track
|
|
1184
|
+
* @returns The same object for chaining
|
|
1185
|
+
*/
|
|
1186
|
+
track(obj) {
|
|
1187
|
+
try {
|
|
1188
|
+
this.sensitiveRefs.push(new WeakRef(obj));
|
|
1189
|
+
} catch {
|
|
1190
|
+
}
|
|
1191
|
+
return obj;
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Register a cleanup callback to run on context exit.
|
|
1195
|
+
*
|
|
1196
|
+
* @param callback - Function to call during cleanup
|
|
1197
|
+
*/
|
|
1198
|
+
onCleanup(callback) {
|
|
1199
|
+
this.cleanupCallbacks.push(callback);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Internal: Run cleanup routines.
|
|
1203
|
+
*/
|
|
1204
|
+
_cleanup() {
|
|
1205
|
+
for (const callback of this.cleanupCallbacks) {
|
|
1206
|
+
try {
|
|
1207
|
+
callback();
|
|
1208
|
+
} catch {
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
for (const ref of this.sensitiveRefs) {
|
|
1212
|
+
const obj = ref.deref();
|
|
1213
|
+
if (obj !== void 0) {
|
|
1214
|
+
secureZeroMemory(obj);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
this.sensitiveRefs.length = 0;
|
|
1218
|
+
this.cleanupCallbacks.length = 0;
|
|
1219
|
+
const g = global;
|
|
1220
|
+
if (typeof g.gc === "function") {
|
|
1221
|
+
g.gc();
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
var SealedExecution = class _SealedExecution {
|
|
1226
|
+
static defaultAuthority = null;
|
|
1227
|
+
attestation;
|
|
1228
|
+
authority;
|
|
1229
|
+
metadata;
|
|
1230
|
+
context = null;
|
|
1231
|
+
certificate = null;
|
|
1232
|
+
chainOfCustody = null;
|
|
1233
|
+
constructor(options = {}) {
|
|
1234
|
+
this.attestation = options.attestation ?? false;
|
|
1235
|
+
this.metadata = options.metadata ?? {};
|
|
1236
|
+
if (this.attestation) {
|
|
1237
|
+
if (options.authority) {
|
|
1238
|
+
this.authority = options.authority;
|
|
1239
|
+
} else {
|
|
1240
|
+
if (!_SealedExecution.defaultAuthority) {
|
|
1241
|
+
_SealedExecution.defaultAuthority = new AttestationAuthority();
|
|
1242
|
+
}
|
|
1243
|
+
this.authority = _SealedExecution.defaultAuthority;
|
|
1244
|
+
}
|
|
1245
|
+
} else {
|
|
1246
|
+
this.authority = options.authority ?? null;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Enter the sealed execution context.
|
|
1251
|
+
*
|
|
1252
|
+
* Use with try/finally or the run() method for automatic cleanup.
|
|
1253
|
+
*
|
|
1254
|
+
* @returns The sealed context for tracking objects
|
|
1255
|
+
*/
|
|
1256
|
+
enter() {
|
|
1257
|
+
this.context = new SealedContext();
|
|
1258
|
+
if (this.attestation) {
|
|
1259
|
+
this.chainOfCustody = new ChainOfCustody(/* @__PURE__ */ new Date(), "sealed_execution");
|
|
1260
|
+
this.chainOfCustody.addAccess("sealed_execution", "context_enter");
|
|
1261
|
+
}
|
|
1262
|
+
return this.context;
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Exit the sealed execution context and destroy all state.
|
|
1266
|
+
*
|
|
1267
|
+
* @param error - Optional error if exiting due to an exception
|
|
1268
|
+
*/
|
|
1269
|
+
exit(error) {
|
|
1270
|
+
if (!this.context) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (this.chainOfCustody) {
|
|
1274
|
+
const action = error ? "context_exit_error" : "context_exit_normal";
|
|
1275
|
+
this.chainOfCustody.addAccess("sealed_execution", action);
|
|
1276
|
+
}
|
|
1277
|
+
this.context._cleanup();
|
|
1278
|
+
if (this.attestation && this.authority) {
|
|
1279
|
+
const resource = new ResourceInfo(
|
|
1280
|
+
"sealed_compute",
|
|
1281
|
+
this.context.executionId,
|
|
1282
|
+
"TRANSIENT",
|
|
1283
|
+
{
|
|
1284
|
+
started_at: this.context.startedAt.toISOString(),
|
|
1285
|
+
duration_ms: Date.now() - this.context.startedAt.getTime(),
|
|
1286
|
+
error: error ? String(error) : null,
|
|
1287
|
+
...this.metadata
|
|
1288
|
+
}
|
|
1289
|
+
);
|
|
1290
|
+
const cert = DestructionCertificate.create(
|
|
1291
|
+
resource,
|
|
1292
|
+
"memory_zero" /* MEMORY_ZERO */,
|
|
1293
|
+
this.authority.authorityId,
|
|
1294
|
+
this.chainOfCustody
|
|
1295
|
+
);
|
|
1296
|
+
this.certificate = this.authority.signCertificate(cert);
|
|
1297
|
+
}
|
|
1298
|
+
this.context = null;
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Run a function within the sealed context.
|
|
1302
|
+
*
|
|
1303
|
+
* Automatically handles enter/exit and cleanup.
|
|
1304
|
+
*
|
|
1305
|
+
* @param fn - Function to execute within the sealed context
|
|
1306
|
+
* @returns The function's return value
|
|
1307
|
+
*/
|
|
1308
|
+
async run(fn) {
|
|
1309
|
+
const ctx = this.enter();
|
|
1310
|
+
try {
|
|
1311
|
+
const result = await fn(ctx);
|
|
1312
|
+
this.exit();
|
|
1313
|
+
return result;
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
this.exit(error);
|
|
1316
|
+
throw error;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
function sealed(options = {}) {
|
|
1321
|
+
return function(fn) {
|
|
1322
|
+
return async function(...args) {
|
|
1323
|
+
const seal = new SealedExecution({
|
|
1324
|
+
attestation: options.attestation,
|
|
1325
|
+
authority: options.authority,
|
|
1326
|
+
metadata: {
|
|
1327
|
+
function: fn.name || "anonymous",
|
|
1328
|
+
...options.metadata
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
const result = await seal.run(async (ctx) => {
|
|
1332
|
+
for (const arg of args) {
|
|
1333
|
+
if (arg && typeof arg === "object") {
|
|
1334
|
+
ctx.track(arg);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return fn(...args);
|
|
1338
|
+
});
|
|
1339
|
+
if (options.attestation && seal.certificate && result && typeof result === "object") {
|
|
1340
|
+
result["_destruction_certificate"] = seal.certificate.toDict();
|
|
1341
|
+
}
|
|
1342
|
+
return result;
|
|
1343
|
+
};
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/index.ts
|
|
1348
|
+
var VERSION = "0.1.0";
|
|
1349
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1350
|
+
0 && (module.exports = {
|
|
1351
|
+
AttestationAuthority,
|
|
1352
|
+
AttestationError,
|
|
1353
|
+
BackendError,
|
|
1354
|
+
ChainOfCustody,
|
|
1355
|
+
CryptoError,
|
|
1356
|
+
CryptoProvider,
|
|
1357
|
+
DataClassification,
|
|
1358
|
+
DataEncryptionKey,
|
|
1359
|
+
DestructionCertificate,
|
|
1360
|
+
DestructionMethod,
|
|
1361
|
+
EFSFError,
|
|
1362
|
+
EncryptedPayload,
|
|
1363
|
+
EphemeralRecord,
|
|
1364
|
+
EphemeralStore,
|
|
1365
|
+
MemoryBackend,
|
|
1366
|
+
RecordExpiredError,
|
|
1367
|
+
RecordNotFoundError,
|
|
1368
|
+
RedisBackend,
|
|
1369
|
+
ResourceInfo,
|
|
1370
|
+
SealedContext,
|
|
1371
|
+
SealedExecution,
|
|
1372
|
+
TTLViolationError,
|
|
1373
|
+
VERSION,
|
|
1374
|
+
ValidationError,
|
|
1375
|
+
constantTimeCompare,
|
|
1376
|
+
createBackend,
|
|
1377
|
+
getDefaultTTL,
|
|
1378
|
+
getMaxTTL,
|
|
1379
|
+
parseTTL,
|
|
1380
|
+
sealed,
|
|
1381
|
+
secureZeroMemory
|
|
1382
|
+
});
|