@sesamy/capsule-server 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/LICENSE +21 -0
- package/README.md +431 -0
- package/dist/index.d.mts +729 -0
- package/dist/index.d.ts +729 -0
- package/dist/index.js +763 -0
- package/dist/index.mjs +705 -0
- package/package.json +58 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for Capsule server-side encryption.
|
|
3
|
+
*/
|
|
4
|
+
/** Bucket key information */
|
|
5
|
+
interface BucketKey {
|
|
6
|
+
/** Bucket identifier (TOTP counter value) */
|
|
7
|
+
bucketId: string;
|
|
8
|
+
/** 256-bit AES key for this bucket */
|
|
9
|
+
key: Buffer;
|
|
10
|
+
/** When this bucket expires */
|
|
11
|
+
expiresAt: Date;
|
|
12
|
+
}
|
|
13
|
+
/** Key wrapping entry - DEK wrapped with a specific key */
|
|
14
|
+
interface WrappedKey {
|
|
15
|
+
/** The key ID used to wrap (e.g., "premium:123456" or "article:crypto-guide") */
|
|
16
|
+
keyId: string;
|
|
17
|
+
/** Base64-encoded wrapped DEK */
|
|
18
|
+
wrappedDek: string;
|
|
19
|
+
/** When this wrapped key expires (for time-bucket keys) - ISO string */
|
|
20
|
+
expiresAt?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Encrypted article with envelope encryption */
|
|
23
|
+
interface EncryptedArticle {
|
|
24
|
+
/** Unique article identifier */
|
|
25
|
+
articleId: string;
|
|
26
|
+
/** Base64-encoded encrypted content (AES-256-GCM ciphertext + auth tag) */
|
|
27
|
+
encryptedContent: string;
|
|
28
|
+
/** Base64-encoded IV used for encryption */
|
|
29
|
+
iv: string;
|
|
30
|
+
/** Multiple wrapped versions of the content DEK for different unlock paths */
|
|
31
|
+
wrappedKeys: WrappedKey[];
|
|
32
|
+
}
|
|
33
|
+
/** Configuration for key wrapping */
|
|
34
|
+
interface KeyWrapConfig {
|
|
35
|
+
/** Key ID (e.g., "premium", "article:crypto-guide") */
|
|
36
|
+
keyId: string;
|
|
37
|
+
/** 256-bit AES key-wrapping key */
|
|
38
|
+
key: Buffer;
|
|
39
|
+
/** Expiration time (for time-bucket keys) */
|
|
40
|
+
expiresAt?: Date;
|
|
41
|
+
}
|
|
42
|
+
/** Options for the CMS encryptor */
|
|
43
|
+
interface CmsEncryptorOptions {
|
|
44
|
+
/** Subscription server URL (for API mode) */
|
|
45
|
+
subscriptionServerUrl?: string;
|
|
46
|
+
/** API key for subscription server authentication */
|
|
47
|
+
apiKey?: string;
|
|
48
|
+
/** Master secret for TOTP mode (base64 encoded) */
|
|
49
|
+
masterSecret?: string;
|
|
50
|
+
/** Bucket period in seconds (default: 30) */
|
|
51
|
+
bucketPeriodSeconds?: number;
|
|
52
|
+
}
|
|
53
|
+
/** Subscription server client options */
|
|
54
|
+
interface SubscriptionClientOptions {
|
|
55
|
+
/** Subscription server base URL */
|
|
56
|
+
serverUrl: string;
|
|
57
|
+
/** API key for authentication */
|
|
58
|
+
apiKey: string;
|
|
59
|
+
}
|
|
60
|
+
/** Response from subscription server for bucket keys */
|
|
61
|
+
interface BucketKeysResponse {
|
|
62
|
+
/** Current bucket key */
|
|
63
|
+
current: BucketKey;
|
|
64
|
+
/** Next bucket key (for clock drift handling) */
|
|
65
|
+
next: BucketKey;
|
|
66
|
+
}
|
|
67
|
+
/** Response from unlocking with a user's public key */
|
|
68
|
+
interface UnlockResponse {
|
|
69
|
+
/** Base64-encoded RSA-OAEP wrapped DEK */
|
|
70
|
+
encryptedDek: string;
|
|
71
|
+
/** Key ID that was used */
|
|
72
|
+
keyId: string;
|
|
73
|
+
/** Bucket ID (for time-bucket keys) */
|
|
74
|
+
bucketId?: string;
|
|
75
|
+
/** When the client should re-request (bucket expiration) */
|
|
76
|
+
expiresAt: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Capsule CMS Server - High-Level API
|
|
81
|
+
*
|
|
82
|
+
* Provides a simple interface for server-side content encryption.
|
|
83
|
+
* The CMS just works with key IDs - it doesn't know or care about
|
|
84
|
+
* tiers, subscriptions, or how keys are derived.
|
|
85
|
+
*
|
|
86
|
+
* Keys are fetched via an async `getKeys` function that you provide.
|
|
87
|
+
* This could:
|
|
88
|
+
* - Fetch from your subscription server
|
|
89
|
+
* - Use TOTP derivation (see `createTotpKeyProvider`)
|
|
90
|
+
* - Return hardcoded/cached keys
|
|
91
|
+
*
|
|
92
|
+
* @example Basic Usage with Custom Key Provider
|
|
93
|
+
* ```typescript
|
|
94
|
+
* import { createCmsServer } from '@sesamy/capsule-server';
|
|
95
|
+
*
|
|
96
|
+
* const cms = createCmsServer({
|
|
97
|
+
* getKeys: async (keyIds) => {
|
|
98
|
+
* // Fetch keys from your subscription server
|
|
99
|
+
* const response = await fetch('/api/keys', {
|
|
100
|
+
* method: 'POST',
|
|
101
|
+
* body: JSON.stringify({ keyIds }),
|
|
102
|
+
* });
|
|
103
|
+
* return response.json();
|
|
104
|
+
* },
|
|
105
|
+
* });
|
|
106
|
+
*
|
|
107
|
+
* // Encrypt with specific key IDs
|
|
108
|
+
* const encrypted = await cms.encrypt('article-123', content, {
|
|
109
|
+
* keyIds: ['premium', 'enterprise'],
|
|
110
|
+
* });
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* @example Using TOTP Key Provider
|
|
114
|
+
* ```typescript
|
|
115
|
+
* import { createCmsServer, createTotpKeyProvider } from '@sesamy/capsule-server';
|
|
116
|
+
*
|
|
117
|
+
* const totp = createTotpKeyProvider({
|
|
118
|
+
* masterSecret: process.env.MASTER_SECRET,
|
|
119
|
+
* bucketPeriodSeconds: 30,
|
|
120
|
+
* });
|
|
121
|
+
*
|
|
122
|
+
* const cms = createCmsServer({
|
|
123
|
+
* getKeys: (keyIds) => totp.getKeys(keyIds),
|
|
124
|
+
* });
|
|
125
|
+
*
|
|
126
|
+
* const encrypted = await cms.encrypt('article-123', content, {
|
|
127
|
+
* keyIds: ['premium', 'enterprise'],
|
|
128
|
+
* });
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A key entry returned by the key provider.
|
|
134
|
+
*/
|
|
135
|
+
interface KeyEntry {
|
|
136
|
+
/** Unique key identifier */
|
|
137
|
+
keyId: string;
|
|
138
|
+
/** 256-bit AES key (Buffer or base64 string) */
|
|
139
|
+
key: Buffer | string;
|
|
140
|
+
/** Optional expiration time (for time-bucket keys) */
|
|
141
|
+
expiresAt?: Date | string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Async function to fetch keys for given key IDs.
|
|
145
|
+
*
|
|
146
|
+
* The CMS calls this with the key IDs it needs, and you return
|
|
147
|
+
* the actual keys. This decouples key management from encryption.
|
|
148
|
+
*
|
|
149
|
+
* @param keyIds - Array of key IDs to fetch
|
|
150
|
+
* @returns Array of key entries with the actual keys
|
|
151
|
+
*/
|
|
152
|
+
type KeyProvider = (keyIds: string[]) => Promise<KeyEntry[]>;
|
|
153
|
+
/**
|
|
154
|
+
* Options for creating a CMS server.
|
|
155
|
+
*/
|
|
156
|
+
interface CmsServerOptions {
|
|
157
|
+
/**
|
|
158
|
+
* Async function to fetch keys for given key IDs.
|
|
159
|
+
*
|
|
160
|
+
* @example Fetch from subscription server
|
|
161
|
+
* ```typescript
|
|
162
|
+
* getKeys: async (keyIds) => {
|
|
163
|
+
* const response = await fetch('/api/keys', {
|
|
164
|
+
* method: 'POST',
|
|
165
|
+
* body: JSON.stringify({ keyIds }),
|
|
166
|
+
* });
|
|
167
|
+
* return response.json();
|
|
168
|
+
* }
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* @example Use TOTP provider
|
|
172
|
+
* ```typescript
|
|
173
|
+
* const totp = createTotpKeyProvider({ masterSecret: '...' });
|
|
174
|
+
* getKeys: (keyIds) => totp.getKeys(keyIds)
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
getKeys: KeyProvider;
|
|
178
|
+
/**
|
|
179
|
+
* Optional logger function for debugging.
|
|
180
|
+
*/
|
|
181
|
+
logger?: (message: string, level: "info" | "warn" | "error") => void;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Options for the encrypt() method.
|
|
185
|
+
*/
|
|
186
|
+
interface EncryptOptions {
|
|
187
|
+
/**
|
|
188
|
+
* Key IDs to encrypt with. The DEK will be wrapped with each key.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```typescript
|
|
192
|
+
* keyIds: ['premium', 'enterprise', 'promo-2024']
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
keyIds: string[];
|
|
196
|
+
/**
|
|
197
|
+
* Output format:
|
|
198
|
+
* - 'json': Returns EncryptedArticle object (default)
|
|
199
|
+
* - 'html': Returns HTML element with data-capsule attribute
|
|
200
|
+
* - 'html-template': Returns just the JSON for templates
|
|
201
|
+
*/
|
|
202
|
+
format?: "json" | "html" | "html-template";
|
|
203
|
+
/** HTML element tag when format is 'html'. Default: 'div' */
|
|
204
|
+
htmlTag?: string;
|
|
205
|
+
/** HTML element class when format is 'html'. */
|
|
206
|
+
htmlClass?: string;
|
|
207
|
+
/** Placeholder content shown before unlock when format is 'html'. */
|
|
208
|
+
placeholder?: string;
|
|
209
|
+
}
|
|
210
|
+
/** Result type based on format option */
|
|
211
|
+
type EncryptResult<T extends EncryptOptions["format"]> = T extends "html" ? string : T extends "html-template" ? string : EncryptedArticle;
|
|
212
|
+
/**
|
|
213
|
+
* CMS Server for content encryption.
|
|
214
|
+
*
|
|
215
|
+
* Encrypts content with envelope encryption - the content is encrypted
|
|
216
|
+
* once with a unique DEK (Data Encryption Key), then the DEK is wrapped
|
|
217
|
+
* with multiple key-wrapping keys so different users can unlock it.
|
|
218
|
+
*
|
|
219
|
+
* @see createCmsServer for the recommended way to create an instance
|
|
220
|
+
*/
|
|
221
|
+
declare class CmsServer {
|
|
222
|
+
private getKeys;
|
|
223
|
+
private logger;
|
|
224
|
+
constructor(options: CmsServerOptions);
|
|
225
|
+
/**
|
|
226
|
+
* Encrypt content with envelope encryption.
|
|
227
|
+
*
|
|
228
|
+
* The content is encrypted once with a unique DEK, then the DEK is wrapped
|
|
229
|
+
* with multiple key-wrapping keys (one for each keyId).
|
|
230
|
+
*
|
|
231
|
+
* @param articleId - Unique article identifier
|
|
232
|
+
* @param content - Plaintext content to encrypt
|
|
233
|
+
* @param options - Encryption options
|
|
234
|
+
* @returns Encrypted article data
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const encrypted = await cms.encrypt('article-123', '<p>Premium content...</p>', {
|
|
239
|
+
* keyIds: ['premium', 'enterprise'],
|
|
240
|
+
* });
|
|
241
|
+
* ```
|
|
242
|
+
*
|
|
243
|
+
* Returns (format: 'json'):
|
|
244
|
+
* ```json
|
|
245
|
+
* {
|
|
246
|
+
* "articleId": "article-123",
|
|
247
|
+
* "encryptedContent": "base64...", // AES-256-GCM encrypted content
|
|
248
|
+
* "iv": "base64...", // 12-byte initialization vector
|
|
249
|
+
* "wrappedKeys": [
|
|
250
|
+
* {
|
|
251
|
+
* "keyId": "premium:1737158400",
|
|
252
|
+
* "wrappedDek": "base64...", // DEK wrapped with this key
|
|
253
|
+
* "expiresAt": "2025-01-18T01:00:00.000Z"
|
|
254
|
+
* },
|
|
255
|
+
* {
|
|
256
|
+
* "keyId": "premium:1737158430",
|
|
257
|
+
* "wrappedDek": "base64...",
|
|
258
|
+
* "expiresAt": "2025-01-18T01:00:30.000Z"
|
|
259
|
+
* }
|
|
260
|
+
* ]
|
|
261
|
+
* }
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
encrypt<T extends EncryptOptions["format"] = "json">(articleId: string, content: string, options: EncryptOptions & {
|
|
265
|
+
format?: T;
|
|
266
|
+
}): Promise<EncryptResult<T>>;
|
|
267
|
+
/**
|
|
268
|
+
* Encrypt and return data in multiple formats for templates.
|
|
269
|
+
*
|
|
270
|
+
* @returns Object with all template formats:
|
|
271
|
+
* - data: The EncryptedArticle object
|
|
272
|
+
* - json: JSON string
|
|
273
|
+
* - attribute: HTML-escaped JSON for data attributes
|
|
274
|
+
* - html: Complete HTML element
|
|
275
|
+
*/
|
|
276
|
+
encryptForTemplate(articleId: string, content: string, options: Omit<EncryptOptions, "format">): Promise<{
|
|
277
|
+
data: EncryptedArticle;
|
|
278
|
+
json: string;
|
|
279
|
+
attribute: string;
|
|
280
|
+
html: string;
|
|
281
|
+
}>;
|
|
282
|
+
/**
|
|
283
|
+
* Escape HTML special characters for safe attribute embedding.
|
|
284
|
+
*/
|
|
285
|
+
private escapeHtml;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Create a CMS server for content encryption.
|
|
289
|
+
*
|
|
290
|
+
* @example With subscription server
|
|
291
|
+
* ```typescript
|
|
292
|
+
* const cms = createCmsServer({
|
|
293
|
+
* getKeys: async (keyIds) => {
|
|
294
|
+
* const response = await fetch('/api/keys', {
|
|
295
|
+
* method: 'POST',
|
|
296
|
+
* body: JSON.stringify({ keyIds }),
|
|
297
|
+
* });
|
|
298
|
+
* return response.json();
|
|
299
|
+
* },
|
|
300
|
+
* });
|
|
301
|
+
* ```
|
|
302
|
+
*
|
|
303
|
+
* @example With TOTP key provider
|
|
304
|
+
* ```typescript
|
|
305
|
+
* const totp = createTotpKeyProvider({ masterSecret: process.env.MASTER_SECRET });
|
|
306
|
+
* const cms = createCmsServer({
|
|
307
|
+
* getKeys: (keyIds) => totp.getKeys(keyIds),
|
|
308
|
+
* });
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
declare function createCmsServer(options: CmsServerOptions): CmsServer;
|
|
312
|
+
/**
|
|
313
|
+
* Options for the TOTP key provider.
|
|
314
|
+
*/
|
|
315
|
+
interface TotpKeyProviderOptions {
|
|
316
|
+
/**
|
|
317
|
+
* Master secret for key derivation.
|
|
318
|
+
* Can be a Buffer or base64-encoded string.
|
|
319
|
+
*/
|
|
320
|
+
masterSecret: Buffer | string;
|
|
321
|
+
/**
|
|
322
|
+
* Bucket period in seconds.
|
|
323
|
+
* Default: 30 seconds.
|
|
324
|
+
*/
|
|
325
|
+
bucketPeriodSeconds?: number;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* TOTP-based key provider.
|
|
329
|
+
*
|
|
330
|
+
* Derives time-bucket keys from a master secret using HKDF.
|
|
331
|
+
* For each key ID, returns BOTH current and next bucket keys
|
|
332
|
+
* to handle clock drift between CMS and subscription server.
|
|
333
|
+
*/
|
|
334
|
+
declare class TotpKeyProvider {
|
|
335
|
+
private masterSecret;
|
|
336
|
+
private bucketPeriodSeconds;
|
|
337
|
+
constructor(options: TotpKeyProviderOptions);
|
|
338
|
+
/**
|
|
339
|
+
* Get keys for the given key IDs.
|
|
340
|
+
*
|
|
341
|
+
* For each keyId, returns two keys:
|
|
342
|
+
* - Current bucket key (e.g., "premium:1737158400")
|
|
343
|
+
* - Next bucket key (e.g., "premium:1737158430")
|
|
344
|
+
*
|
|
345
|
+
* This ensures content encrypted near a bucket boundary
|
|
346
|
+
* can still be decrypted after the bucket rotates.
|
|
347
|
+
*/
|
|
348
|
+
getKeys(keyIds: string[]): Promise<KeyEntry[]>;
|
|
349
|
+
/**
|
|
350
|
+
* Derive a static key for an article (no time bucket).
|
|
351
|
+
* Useful for per-article purchase access.
|
|
352
|
+
*/
|
|
353
|
+
getArticleKey(articleId: string): Promise<KeyEntry>;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Create a TOTP key provider for deriving time-bucket keys.
|
|
357
|
+
*
|
|
358
|
+
* Use this with CmsServer when you want to derive keys locally
|
|
359
|
+
* from a shared master secret (no API calls needed).
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```typescript
|
|
363
|
+
* const totp = createTotpKeyProvider({
|
|
364
|
+
* masterSecret: process.env.MASTER_SECRET,
|
|
365
|
+
* bucketPeriodSeconds: 30,
|
|
366
|
+
* });
|
|
367
|
+
*
|
|
368
|
+
* const cms = createCmsServer({
|
|
369
|
+
* getKeys: (keyIds) => totp.getKeys(keyIds),
|
|
370
|
+
* });
|
|
371
|
+
*
|
|
372
|
+
* // Or combine with article keys:
|
|
373
|
+
* const cms = createCmsServer({
|
|
374
|
+
* getKeys: async (keyIds) => {
|
|
375
|
+
* const keys = await totp.getKeys(keyIds);
|
|
376
|
+
* // Add article key if requested
|
|
377
|
+
* if (keyIds.some(id => id.startsWith('article:'))) {
|
|
378
|
+
* const articleId = keyIds.find(id => id.startsWith('article:'))!.slice(8);
|
|
379
|
+
* keys.push(await totp.getArticleKey(articleId));
|
|
380
|
+
* }
|
|
381
|
+
* return keys;
|
|
382
|
+
* },
|
|
383
|
+
* });
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
declare function createTotpKeyProvider(options: TotpKeyProviderOptions): TotpKeyProvider;
|
|
387
|
+
/** @deprecated Use CmsServer instead */
|
|
388
|
+
declare const CapsuleServer: typeof CmsServer;
|
|
389
|
+
/** @deprecated Use CmsServerOptions instead */
|
|
390
|
+
type CapsuleServerOptions = CmsServerOptions;
|
|
391
|
+
/** @deprecated Use createCmsServer instead */
|
|
392
|
+
declare const createCapsule: typeof createCmsServer;
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* CMS Content Encryptor
|
|
396
|
+
*
|
|
397
|
+
* Provides envelope encryption for article content:
|
|
398
|
+
* 1. Content is encrypted ONCE with a unique DEK (AES-256-GCM)
|
|
399
|
+
* 2. The DEK is wrapped with MULTIPLE key-wrapping keys for different unlock paths
|
|
400
|
+
*
|
|
401
|
+
* Supports two modes for obtaining bucket keys:
|
|
402
|
+
* - TOTP: Derive keys locally from master secret (no network calls)
|
|
403
|
+
* - API: Fetch keys from subscription server
|
|
404
|
+
*/
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* CMS Content Encryptor for Capsule.
|
|
408
|
+
*
|
|
409
|
+
* Use this in your CMS to encrypt article content with envelope encryption.
|
|
410
|
+
*/
|
|
411
|
+
declare class CmsEncryptor {
|
|
412
|
+
private masterSecret;
|
|
413
|
+
private subscriptionServerUrl;
|
|
414
|
+
private apiKey;
|
|
415
|
+
private bucketPeriodSeconds;
|
|
416
|
+
constructor(options?: CmsEncryptorOptions);
|
|
417
|
+
/**
|
|
418
|
+
* Get bucket keys for a key ID.
|
|
419
|
+
*
|
|
420
|
+
* In TOTP mode: derives from master secret locally.
|
|
421
|
+
* In API mode: fetches from subscription server.
|
|
422
|
+
*/
|
|
423
|
+
getBucketKeys(keyId: string): Promise<{
|
|
424
|
+
current: BucketKey;
|
|
425
|
+
next: BucketKey;
|
|
426
|
+
}>;
|
|
427
|
+
/**
|
|
428
|
+
* Encrypt article content with envelope encryption.
|
|
429
|
+
*
|
|
430
|
+
* The content is encrypted once with a unique DEK, then the DEK is wrapped
|
|
431
|
+
* with multiple key-wrapping keys for different unlock paths.
|
|
432
|
+
*
|
|
433
|
+
* @param articleId - Unique article identifier
|
|
434
|
+
* @param content - Plaintext content to encrypt
|
|
435
|
+
* @param keyConfigs - Array of key-wrapping configurations
|
|
436
|
+
* @returns Encrypted article with wrapped keys
|
|
437
|
+
*/
|
|
438
|
+
encryptArticle(articleId: string, content: string, keyConfigs: KeyWrapConfig[]): EncryptedArticle;
|
|
439
|
+
/**
|
|
440
|
+
* Encrypt article with tier-based time-bucket keys.
|
|
441
|
+
*
|
|
442
|
+
* Automatically gets current and next bucket keys for the specified tier,
|
|
443
|
+
* plus any additional static keys (e.g., per-article keys).
|
|
444
|
+
*
|
|
445
|
+
* @param articleId - Unique article identifier
|
|
446
|
+
* @param content - Plaintext content to encrypt
|
|
447
|
+
* @param tier - Subscription tier (e.g., "premium")
|
|
448
|
+
* @param additionalKeys - Optional additional key configurations (e.g., per-article keys)
|
|
449
|
+
*/
|
|
450
|
+
encryptArticleWithTier(articleId: string, content: string, tier: string, additionalKeys?: KeyWrapConfig[]): Promise<EncryptedArticle>;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Create a simple encryptor for TOTP mode.
|
|
454
|
+
*/
|
|
455
|
+
declare function createTotpEncryptor(masterSecret: string | Buffer, bucketPeriodSeconds?: number): CmsEncryptor;
|
|
456
|
+
/**
|
|
457
|
+
* Create an encryptor that fetches keys from subscription server.
|
|
458
|
+
*/
|
|
459
|
+
declare function createApiEncryptor(subscriptionServerUrl: string, apiKey: string): CmsEncryptor;
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Subscription Server Utilities
|
|
463
|
+
*
|
|
464
|
+
* For building subscription server endpoints that:
|
|
465
|
+
* 1. Provide bucket keys to CMS (for encryption)
|
|
466
|
+
* 2. Unwrap DEKs for authenticated users (for decryption)
|
|
467
|
+
*
|
|
468
|
+
* @example
|
|
469
|
+
* ```typescript
|
|
470
|
+
* import { createSubscriptionServer } from '@sesamy/capsule-server';
|
|
471
|
+
*
|
|
472
|
+
* const server = createSubscriptionServer({
|
|
473
|
+
* masterSecret: process.env.MASTER_SECRET,
|
|
474
|
+
* bucketPeriodSeconds: 30,
|
|
475
|
+
* });
|
|
476
|
+
*
|
|
477
|
+
* // Endpoint for users to unlock content
|
|
478
|
+
* app.post('/api/unlock', async (req, res) => {
|
|
479
|
+
* const { keyId, wrappedDek, publicKey } = req.body;
|
|
480
|
+
* const result = await server.unlockForUser({ keyId, wrappedDek }, publicKey);
|
|
481
|
+
* res.json(result);
|
|
482
|
+
* });
|
|
483
|
+
* ```
|
|
484
|
+
*/
|
|
485
|
+
|
|
486
|
+
/** Options for creating a subscription server */
|
|
487
|
+
interface SubscriptionServerOptions {
|
|
488
|
+
/** Master secret for deriving bucket keys (base64 encoded string or Buffer) */
|
|
489
|
+
masterSecret: string | Buffer;
|
|
490
|
+
/** Bucket period in seconds (default: 30) */
|
|
491
|
+
bucketPeriodSeconds?: number;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Subscription Server for Capsule.
|
|
495
|
+
*
|
|
496
|
+
* Manages master secret and provides:
|
|
497
|
+
* - Bucket keys for CMS (time-limited)
|
|
498
|
+
* - DEK unwrapping for authenticated users
|
|
499
|
+
*
|
|
500
|
+
* @see createSubscriptionServer for the recommended way to create an instance
|
|
501
|
+
*/
|
|
502
|
+
declare class SubscriptionServer {
|
|
503
|
+
private masterSecret;
|
|
504
|
+
private bucketPeriodSeconds;
|
|
505
|
+
constructor(options: SubscriptionServerOptions);
|
|
506
|
+
/**
|
|
507
|
+
* Get bucket keys for a key ID (for CMS).
|
|
508
|
+
*
|
|
509
|
+
* Returns current and next bucket keys so CMS can encrypt
|
|
510
|
+
* content that works across bucket boundaries.
|
|
511
|
+
*/
|
|
512
|
+
getBucketKeysForCms(keyId: string): {
|
|
513
|
+
current: BucketKey;
|
|
514
|
+
next: BucketKey;
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* Get bucket keys formatted for API response.
|
|
518
|
+
*/
|
|
519
|
+
getBucketKeysResponse(keyId: string): {
|
|
520
|
+
current: {
|
|
521
|
+
bucketId: string;
|
|
522
|
+
key: string;
|
|
523
|
+
expiresAt: string;
|
|
524
|
+
};
|
|
525
|
+
next: {
|
|
526
|
+
bucketId: string;
|
|
527
|
+
key: string;
|
|
528
|
+
expiresAt: string;
|
|
529
|
+
};
|
|
530
|
+
};
|
|
531
|
+
/**
|
|
532
|
+
* Validate that a bucket ID is current or adjacent.
|
|
533
|
+
*/
|
|
534
|
+
isBucketValid(bucketId: string): boolean;
|
|
535
|
+
/**
|
|
536
|
+
* Unwrap a DEK and re-wrap it with a user's RSA public key.
|
|
537
|
+
*
|
|
538
|
+
* This is the core unlock operation:
|
|
539
|
+
* 1. Parse the wrapped key to extract keyId and bucket info
|
|
540
|
+
* 2. Derive the key-wrapping key from master secret
|
|
541
|
+
* 3. Unwrap the DEK
|
|
542
|
+
* 4. Re-wrap with user's RSA public key
|
|
543
|
+
*
|
|
544
|
+
* @param wrappedKey - The wrapped key entry from the article
|
|
545
|
+
* @param userPublicKeyB64 - User's RSA public key (Base64 SPKI format)
|
|
546
|
+
* @param staticKeyLookup - Optional function to look up static keys (for per-article keys). Can be sync or async.
|
|
547
|
+
*/
|
|
548
|
+
unlockForUser(wrappedKey: WrappedKey, userPublicKeyB64: string, staticKeyLookup?: (keyId: string) => Buffer | null | Promise<Buffer | null>): Promise<UnlockResponse>;
|
|
549
|
+
/**
|
|
550
|
+
* Internal helper to unwrap DEK and re-wrap with user's public key.
|
|
551
|
+
*/
|
|
552
|
+
private unwrapAndRewrap;
|
|
553
|
+
/**
|
|
554
|
+
* Simple unlock when you already have the key-wrapping key.
|
|
555
|
+
* Used when the unlock logic is separate from bucket key derivation.
|
|
556
|
+
*/
|
|
557
|
+
wrapDekForUser(dek: Buffer, userPublicKeyB64: string, keyId: string, expiresAt: Date): UnlockResponse;
|
|
558
|
+
/**
|
|
559
|
+
* Get the key-wrapping key for a bucket key ID.
|
|
560
|
+
* Useful when you need the raw key for custom logic.
|
|
561
|
+
*/
|
|
562
|
+
getBucketKey(keyId: string, bucketId: string): Buffer;
|
|
563
|
+
/**
|
|
564
|
+
* Get the key-wrapping key for a tier, wrapped with user's RSA public key.
|
|
565
|
+
*
|
|
566
|
+
* This enables "unlock once, access all" for tier content:
|
|
567
|
+
* - Client receives the AES-KW key (not the DEK)
|
|
568
|
+
* - Client can unwrap any article's DEK locally
|
|
569
|
+
* - No per-article unlock requests needed
|
|
570
|
+
*
|
|
571
|
+
* @param tier - The tier name (e.g., "premium")
|
|
572
|
+
* @param bucketId - The bucket ID to get the key for
|
|
573
|
+
* @param userPublicKeyB64 - User's RSA public key (Base64 SPKI format)
|
|
574
|
+
*/
|
|
575
|
+
getTierKeyForUser(tier: string, bucketId: string, userPublicKeyB64: string): UnlockResponse;
|
|
576
|
+
/**
|
|
577
|
+
* Convert Base64 SPKI to PEM format for Node.js crypto.
|
|
578
|
+
*/
|
|
579
|
+
private convertToPem;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Create a subscription server for handling unlock requests.
|
|
583
|
+
*
|
|
584
|
+
* @example
|
|
585
|
+
* ```typescript
|
|
586
|
+
* const server = createSubscriptionServer({
|
|
587
|
+
* masterSecret: process.env.MASTER_SECRET,
|
|
588
|
+
* bucketPeriodSeconds: 30,
|
|
589
|
+
* });
|
|
590
|
+
*
|
|
591
|
+
* app.post('/api/unlock', async (req, res) => {
|
|
592
|
+
* const { keyId, wrappedDek, publicKey } = req.body;
|
|
593
|
+
* const result = await server.unlockForUser({ keyId, wrappedDek }, publicKey);
|
|
594
|
+
* res.json(result);
|
|
595
|
+
* });
|
|
596
|
+
* ```
|
|
597
|
+
*/
|
|
598
|
+
declare function createSubscriptionServer(options: SubscriptionServerOptions): SubscriptionServer;
|
|
599
|
+
/**
|
|
600
|
+
* Create a subscription server (legacy signature).
|
|
601
|
+
* @deprecated Use createSubscriptionServer({ masterSecret, bucketPeriodSeconds }) instead
|
|
602
|
+
*/
|
|
603
|
+
declare function createSubscriptionServer(masterSecret: string | Buffer, bucketPeriodSeconds?: number): SubscriptionServer;
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* AES-256-GCM encryption utilities.
|
|
607
|
+
*
|
|
608
|
+
* Provides content encryption with unique DEKs and key wrapping.
|
|
609
|
+
*/
|
|
610
|
+
/** GCM IV size in bytes (96 bits as recommended by NIST) */
|
|
611
|
+
declare const GCM_IV_SIZE = 12;
|
|
612
|
+
/** GCM authentication tag length in bytes */
|
|
613
|
+
declare const GCM_TAG_LENGTH = 16;
|
|
614
|
+
/** AES-256 key size in bytes */
|
|
615
|
+
declare const AES_KEY_SIZE = 32;
|
|
616
|
+
/**
|
|
617
|
+
* Generate a random 256-bit AES key (DEK).
|
|
618
|
+
*/
|
|
619
|
+
declare function generateDek(): Buffer;
|
|
620
|
+
/**
|
|
621
|
+
* Generate a random IV for AES-GCM.
|
|
622
|
+
*/
|
|
623
|
+
declare function generateIv(): Buffer;
|
|
624
|
+
/**
|
|
625
|
+
* Encrypt content with AES-256-GCM.
|
|
626
|
+
*
|
|
627
|
+
* @param content - Plaintext content to encrypt
|
|
628
|
+
* @param dek - 256-bit AES key
|
|
629
|
+
* @param iv - 96-bit initialization vector (generated if not provided)
|
|
630
|
+
* @returns Encrypted content (ciphertext + auth tag) and IV
|
|
631
|
+
*/
|
|
632
|
+
declare function encryptContent(content: string | Buffer, dek: Buffer, iv?: Buffer): {
|
|
633
|
+
encryptedContent: Buffer;
|
|
634
|
+
iv: Buffer;
|
|
635
|
+
};
|
|
636
|
+
/**
|
|
637
|
+
* Decrypt content with AES-256-GCM.
|
|
638
|
+
*
|
|
639
|
+
* @param encryptedContent - Ciphertext + auth tag
|
|
640
|
+
* @param dek - 256-bit AES key
|
|
641
|
+
* @param iv - Initialization vector used for encryption
|
|
642
|
+
* @returns Decrypted plaintext
|
|
643
|
+
*/
|
|
644
|
+
declare function decryptContent(encryptedContent: Buffer, dek: Buffer, iv: Buffer): Buffer;
|
|
645
|
+
/**
|
|
646
|
+
* Wrap (encrypt) a DEK with a key-wrapping key using AES-256-GCM.
|
|
647
|
+
*
|
|
648
|
+
* This is used to create multiple wrapped versions of the same DEK,
|
|
649
|
+
* each encrypted with a different key-wrapping key.
|
|
650
|
+
*
|
|
651
|
+
* @param dek - The data encryption key to wrap
|
|
652
|
+
* @param wrappingKey - The key-wrapping key (256-bit AES)
|
|
653
|
+
* @returns Wrapped DEK (IV + ciphertext + auth tag)
|
|
654
|
+
*/
|
|
655
|
+
declare function wrapDek(dek: Buffer, wrappingKey: Buffer): Buffer;
|
|
656
|
+
/**
|
|
657
|
+
* Unwrap (decrypt) a DEK with a key-wrapping key.
|
|
658
|
+
*
|
|
659
|
+
* @param wrappedDek - The wrapped DEK (IV + ciphertext + auth tag)
|
|
660
|
+
* @param wrappingKey - The key-wrapping key used to wrap
|
|
661
|
+
* @returns The unwrapped DEK
|
|
662
|
+
*/
|
|
663
|
+
declare function unwrapDek(wrappedDek: Buffer, wrappingKey: Buffer): Buffer;
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Time-bucket key derivation using HKDF (TOTP-style).
|
|
667
|
+
*
|
|
668
|
+
* Derives deterministic AES-256 keys from a master secret and time bucket ID.
|
|
669
|
+
* Keys rotate every `bucketPeriodSeconds` (default: 30 seconds like TOTP).
|
|
670
|
+
*/
|
|
671
|
+
|
|
672
|
+
/** Default bucket period in seconds (30s like TOTP) */
|
|
673
|
+
declare const DEFAULT_BUCKET_PERIOD_SECONDS = 30;
|
|
674
|
+
/**
|
|
675
|
+
* HKDF key derivation function (RFC 5869).
|
|
676
|
+
* Derives a key from input keying material using HMAC-SHA256.
|
|
677
|
+
*/
|
|
678
|
+
declare function hkdf(ikm: Buffer, salt: Buffer | string, info: Buffer | string, length: number): Buffer;
|
|
679
|
+
/**
|
|
680
|
+
* Get the bucket ID for a given timestamp.
|
|
681
|
+
*/
|
|
682
|
+
declare function getBucketId(timestampMs?: number, bucketPeriodSeconds?: number): string;
|
|
683
|
+
/**
|
|
684
|
+
* Get the current bucket ID.
|
|
685
|
+
*/
|
|
686
|
+
declare function getCurrentBucket(bucketPeriodSeconds?: number): string;
|
|
687
|
+
/**
|
|
688
|
+
* Get the next bucket ID.
|
|
689
|
+
*/
|
|
690
|
+
declare function getNextBucket(bucketPeriodSeconds?: number): string;
|
|
691
|
+
/**
|
|
692
|
+
* Get the previous bucket ID.
|
|
693
|
+
*/
|
|
694
|
+
declare function getPreviousBucket(bucketPeriodSeconds?: number): string;
|
|
695
|
+
/**
|
|
696
|
+
* Get when a bucket expires.
|
|
697
|
+
*/
|
|
698
|
+
declare function getBucketExpiration(bucketId: string, bucketPeriodSeconds?: number): Date;
|
|
699
|
+
/**
|
|
700
|
+
* Check if a bucket is currently valid (current, next, or previous for grace period).
|
|
701
|
+
*/
|
|
702
|
+
declare function isBucketValid(bucketId: string, bucketPeriodSeconds?: number): boolean;
|
|
703
|
+
/**
|
|
704
|
+
* Derive a bucket key from master secret + bucket ID using HKDF.
|
|
705
|
+
*
|
|
706
|
+
* @param masterSecret - The master secret (256-bit)
|
|
707
|
+
* @param keyId - The key identifier (e.g., tier name like "premium")
|
|
708
|
+
* @param bucketId - The bucket identifier
|
|
709
|
+
* @returns 256-bit AES key
|
|
710
|
+
*/
|
|
711
|
+
declare function deriveBucketKey(masterSecret: Buffer, keyId: string, bucketId: string): Buffer;
|
|
712
|
+
/**
|
|
713
|
+
* Get bucket key with metadata.
|
|
714
|
+
*/
|
|
715
|
+
declare function getBucketKey(masterSecret: Buffer, keyId: string, bucketId: string, bucketPeriodSeconds?: number): BucketKey;
|
|
716
|
+
/**
|
|
717
|
+
* Get current and next bucket keys for a key ID.
|
|
718
|
+
* Used by CMS to wrap content DEKs for both time windows.
|
|
719
|
+
*/
|
|
720
|
+
declare function getBucketKeys(masterSecret: Buffer, keyId: string, bucketPeriodSeconds?: number): {
|
|
721
|
+
current: BucketKey;
|
|
722
|
+
next: BucketKey;
|
|
723
|
+
};
|
|
724
|
+
/**
|
|
725
|
+
* Generate a new master secret (256-bit random).
|
|
726
|
+
*/
|
|
727
|
+
declare function generateMasterSecret(): Buffer;
|
|
728
|
+
|
|
729
|
+
export { AES_KEY_SIZE, type BucketKey, type BucketKeysResponse, CapsuleServer, type CapsuleServerOptions, CmsEncryptor, type CmsEncryptorOptions, CmsServer, type CmsServerOptions, DEFAULT_BUCKET_PERIOD_SECONDS, type EncryptOptions, type EncryptedArticle, GCM_IV_SIZE, GCM_TAG_LENGTH, type KeyEntry, type KeyProvider, type KeyWrapConfig, type SubscriptionClientOptions, SubscriptionServer, TotpKeyProvider, type TotpKeyProviderOptions, type UnlockResponse, type WrappedKey, createApiEncryptor, createCapsule, createCmsServer, createSubscriptionServer, createTotpEncryptor, createTotpKeyProvider, decryptContent, deriveBucketKey, encryptContent, generateDek, generateIv, generateMasterSecret, getBucketExpiration, getBucketId, getBucketKey, getBucketKeys, getCurrentBucket, getNextBucket, getPreviousBucket, hkdf, isBucketValid, unwrapDek, wrapDek };
|