@learncard/sss-key-manager 0.1.23 → 0.2.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 (56) hide show
  1. package/README.md +12 -95
  2. package/dist/api-client.d.ts +44 -7
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/atomic-operations.d.ts +10 -2
  5. package/dist/atomic-operations.d.ts.map +1 -1
  6. package/dist/email-relay-crypto.d.ts +49 -0
  7. package/dist/email-relay-crypto.d.ts.map +1 -0
  8. package/dist/escrow-attestation.d.ts +8 -0
  9. package/dist/escrow-attestation.d.ts.map +1 -0
  10. package/dist/escrow-crypto.d.ts +52 -0
  11. package/dist/escrow-crypto.d.ts.map +1 -0
  12. package/dist/escrow-pin.d.ts +22 -0
  13. package/dist/escrow-pin.d.ts.map +1 -0
  14. package/dist/index.d.ts +13 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/key-manager.d.ts +2 -0
  17. package/dist/key-manager.d.ts.map +1 -1
  18. package/dist/qr-login.d.ts.map +1 -1
  19. package/dist/recovery-phrase.d.ts +6 -0
  20. package/dist/recovery-phrase.d.ts.map +1 -1
  21. package/dist/sss-key-manager.cjs.development.js +3163 -440
  22. package/dist/sss-key-manager.cjs.development.js.map +4 -4
  23. package/dist/sss-key-manager.cjs.production.min.js +33 -33
  24. package/dist/sss-key-manager.cjs.production.min.js.map +4 -4
  25. package/dist/sss-key-manager.esm.js +3163 -440
  26. package/dist/sss-key-manager.esm.js.map +4 -4
  27. package/dist/sss-strategy.d.ts +74 -4
  28. package/dist/sss-strategy.d.ts.map +1 -1
  29. package/dist/storage.d.ts +1 -0
  30. package/dist/storage.d.ts.map +1 -1
  31. package/dist/types.d.ts +87 -4
  32. package/dist/types.d.ts.map +1 -1
  33. package/package.json +2 -2
  34. package/src/api-client.test.ts +122 -0
  35. package/src/api-client.ts +107 -116
  36. package/src/atomic-operations.test.ts +56 -16
  37. package/src/atomic-operations.ts +55 -19
  38. package/src/critical-paths.test.ts +51 -1
  39. package/src/email-relay-crypto.test.ts +87 -0
  40. package/src/email-relay-crypto.ts +293 -0
  41. package/src/escrow-attestation.test.ts +85 -0
  42. package/src/escrow-attestation.ts +64 -0
  43. package/src/escrow-crypto.test.ts +381 -0
  44. package/src/escrow-crypto.ts +319 -0
  45. package/src/escrow-pin.test.ts +119 -0
  46. package/src/escrow-pin.ts +83 -0
  47. package/src/index.ts +63 -1
  48. package/src/key-manager.ts +4 -9
  49. package/src/qr-login.ts +22 -15
  50. package/src/recovery-phrase.ts +40 -0
  51. package/src/sss-session-writes.test.ts +109 -0
  52. package/src/sss-strategy.test.ts +2642 -110
  53. package/src/sss-strategy.ts +2186 -224
  54. package/src/storage.test.ts +66 -8
  55. package/src/storage.ts +41 -29
  56. package/src/types.ts +107 -6
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import 'fake-indexeddb/auto';
16
- import { describe, it, expect, beforeEach } from 'vitest';
16
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
17
17
 
18
18
  import {
19
19
  storeDeviceShare,
@@ -25,6 +25,11 @@ import {
25
25
  storeShareVersion,
26
26
  getShareVersion,
27
27
  } from './storage';
28
+ import {
29
+ createSSSStrategy,
30
+ readPendingShareCandidates,
31
+ writePendingShareCandidates,
32
+ } from './sss-strategy';
28
33
 
29
34
  // ---------------------------------------------------------------------------
30
35
  // Helpers
@@ -249,7 +254,7 @@ describe('SSS Storage', () => {
249
254
  expect(await getShareVersion(id)).toBeNull();
250
255
  });
251
256
 
252
- it('deleting one user does not affect another user\'s version', async () => {
257
+ it("deleting one user does not affect another user's version", async () => {
253
258
  const idA = 'sss-device-share:user-a';
254
259
  const idB = 'sss-device-share:user-b';
255
260
 
@@ -380,7 +385,10 @@ describe('SSS Storage', () => {
380
385
  const t = db.transaction('shares', 'readwrite');
381
386
  const s = t.objectStore('shares');
382
387
  // Store garbage data that will fail AES-GCM decryption
383
- const req = s.put({ version: 1, iv: 'bad', cipher: 'bad', keyVersion: 1 }, orphanId);
388
+ const req = s.put(
389
+ { version: 1, iv: 'bad', cipher: 'bad', keyVersion: 1 },
390
+ orphanId
391
+ );
384
392
  req.onsuccess = () => resolve();
385
393
  req.onerror = () => reject(req.error);
386
394
  });
@@ -427,11 +435,7 @@ describe('SSS Storage', () => {
427
435
  // Session-only storage (public computer mode)
428
436
  // ---------------------------------------------------------------------------
429
437
 
430
- import {
431
- isPublicComputerMode,
432
- setPublicComputerMode,
433
- createAdaptiveStorage,
434
- } from './storage';
438
+ import { isPublicComputerMode, setPublicComputerMode, createAdaptiveStorage } from './storage';
435
439
 
436
440
  describe('Session-only storage (public computer mode)', () => {
437
441
  beforeEach(() => {
@@ -528,3 +532,57 @@ describe('Session-only storage (public computer mode)', () => {
528
532
  });
529
533
  });
530
534
  });
535
+
536
+ describe.each([false, true])(
537
+ 'SSS automatic cleanup with adaptive storage (public mode=%s)',
538
+ publicMode => {
539
+ beforeEach(async () => {
540
+ sessionStorage.clear();
541
+ await clearAllShares();
542
+ setPublicComputerMode(publicMode);
543
+ });
544
+
545
+ afterEach(async () => {
546
+ sessionStorage.clear();
547
+ await clearAllShares();
548
+ });
549
+
550
+ it('preserves scoped and unscoped pending writes while clearing stale main shares', async () => {
551
+ const storage = createAdaptiveStorage();
552
+ const scoped = createSSSStrategy({ serverUrl: 'https://unused.test', storage });
553
+ const accountId = 'sss-device-share:account-a';
554
+ const otherId = 'sss-device-share:account-b';
555
+ const scopedPending = [{ share: 'pending-a', createdAt: Date.now() }];
556
+ const legacyPending = [{ share: 'pending-legacy', createdAt: Date.now() }];
557
+ const otherPending = [{ share: 'pending-b', createdAt: Date.now() }];
558
+
559
+ scoped.setActiveUser!('account-a');
560
+ await scoped.storeLocalKey('stale-a');
561
+ await scoped.storeLocalShareVersion!(3);
562
+ await writePendingShareCandidates(storage, scopedPending, accountId);
563
+ await storage.storeDeviceShare('share-b', otherId);
564
+ await storage.storeShareVersion(7, otherId);
565
+ await writePendingShareCandidates(storage, otherPending, otherId);
566
+
567
+ await scoped.clearLocalKeys({ preservePending: true });
568
+ expect(await storage.getDeviceShare(accountId)).toBeNull();
569
+ expect(await storage.getShareVersion(accountId)).toBeNull();
570
+ expect(await readPendingShareCandidates(storage, accountId)).toEqual(scopedPending);
571
+ expect(await scoped.getLocalKey()).toBe('pending-a');
572
+
573
+ const legacy = createSSSStrategy({ serverUrl: 'https://unused.test', storage });
574
+ await legacy.storeLocalKey('stale-legacy');
575
+ await legacy.storeLocalShareVersion!(2);
576
+ await writePendingShareCandidates(storage, legacyPending);
577
+ await legacy.clearLocalKeys({ preservePending: true });
578
+ expect(await storage.getDeviceShare()).toBeNull();
579
+ expect(await storage.getShareVersion()).toBeNull();
580
+ expect(await readPendingShareCandidates(storage)).toEqual(legacyPending);
581
+ expect(await legacy.getLocalKey()).toBe('pending-legacy');
582
+
583
+ expect(await storage.getDeviceShare(otherId)).toBe('share-b');
584
+ expect(await storage.getShareVersion(otherId)).toBe(7);
585
+ expect(await readPendingShareCandidates(storage, otherId)).toEqual(otherPending);
586
+ });
587
+ }
588
+ );
package/src/storage.ts CHANGED
@@ -70,11 +70,10 @@ async function getOrCreateMasterKey(): Promise<CryptoKey> {
70
70
 
71
71
  if (existing) return existing;
72
72
 
73
- const key = await crypto.subtle.generateKey(
74
- { name: 'AES-GCM', length: 256 },
75
- false,
76
- ['encrypt', 'decrypt']
77
- );
73
+ const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, [
74
+ 'encrypt',
75
+ 'decrypt',
76
+ ]);
78
77
 
79
78
  await tx(db, KEYS_STORE, 'readwrite', s => s.put(key, 'master-key'));
80
79
 
@@ -145,7 +144,10 @@ async function decryptShare(payload: EncryptedPayload, id: string): Promise<stri
145
144
  return new TextDecoder().decode(plainBuffer);
146
145
  }
147
146
 
148
- export async function storeDeviceShare(share: string, id: string = DEFAULT_DEVICE_SHARE_ID): Promise<void> {
147
+ export async function storeDeviceShare(
148
+ share: string,
149
+ id: string = DEFAULT_DEVICE_SHARE_ID
150
+ ): Promise<void> {
149
151
  const db = await openDB();
150
152
 
151
153
  try {
@@ -160,7 +162,10 @@ export async function storeDeviceShare(share: string, id: string = DEFAULT_DEVIC
160
162
  * Store the share version alongside a device share.
161
163
  * Stored as a separate key `{id}:version` to avoid changing the encryption format.
162
164
  */
163
- export async function storeShareVersion(version: number, id: string = DEFAULT_DEVICE_SHARE_ID): Promise<void> {
165
+ export async function storeShareVersion(
166
+ version: number,
167
+ id: string = DEFAULT_DEVICE_SHARE_ID
168
+ ): Promise<void> {
164
169
  const db = await openDB();
165
170
 
166
171
  try {
@@ -174,7 +179,9 @@ export async function storeShareVersion(version: number, id: string = DEFAULT_DE
174
179
  * Retrieve the share version for a device share.
175
180
  * Returns null if no version is stored (legacy shares).
176
181
  */
177
- export async function getShareVersion(id: string = DEFAULT_DEVICE_SHARE_ID): Promise<number | null> {
182
+ export async function getShareVersion(
183
+ id: string = DEFAULT_DEVICE_SHARE_ID
184
+ ): Promise<number | null> {
178
185
  const db = await openDB();
179
186
 
180
187
  try {
@@ -192,9 +199,7 @@ export async function getDeviceShare(id: string = DEFAULT_DEVICE_SHARE_ID): Prom
192
199
  const db = await openDB();
193
200
 
194
201
  try {
195
- const raw = await tx<unknown>(db, SHARES_STORE, 'readonly', s =>
196
- s.get(id)
197
- );
202
+ const raw = await tx<unknown>(db, SHARES_STORE, 'readonly', s => s.get(id));
198
203
 
199
204
  if (raw == null) {
200
205
  return null;
@@ -288,7 +293,9 @@ export async function listAllDeviceShares(): Promise<DeviceShareEntry[]> {
288
293
  // Orphaned entry — value exists but can't be decrypted.
289
294
  // Auto-clean it (and its companion :version key) to prevent
290
295
  // phantom "(decrypt failed)" entries from accumulating.
291
- console.warn(`SSS Storage: removing orphaned entry that failed to decrypt: ${id}`);
296
+ console.warn(
297
+ `SSS Storage: removing orphaned entry that failed to decrypt: ${id}`
298
+ );
292
299
  await deleteDeviceShare(id);
293
300
  continue;
294
301
  }
@@ -321,8 +328,10 @@ const SESSION_PREFIX = 'sss:';
321
328
 
322
329
  export function isPublicComputerMode(): boolean {
323
330
  try {
324
- return typeof sessionStorage !== 'undefined' &&
325
- sessionStorage.getItem('lc-session-mode') === 'public';
331
+ return (
332
+ typeof sessionStorage !== 'undefined' &&
333
+ sessionStorage.getItem('lc-session-mode') === 'public'
334
+ );
326
335
  } catch {
327
336
  return false;
328
337
  }
@@ -340,7 +349,10 @@ export function setPublicComputerMode(enabled: boolean): void {
340
349
  }
341
350
  }
342
351
 
343
- async function sessionStoreDeviceShare(share: string, id: string = DEFAULT_DEVICE_SHARE_ID): Promise<void> {
352
+ async function sessionStoreDeviceShare(
353
+ share: string,
354
+ id: string = DEFAULT_DEVICE_SHARE_ID
355
+ ): Promise<void> {
344
356
  sessionStorage.setItem(`${SESSION_PREFIX}${id}`, share);
345
357
  }
346
358
 
@@ -357,11 +369,16 @@ async function sessionDeleteDeviceShare(id: string = DEFAULT_DEVICE_SHARE_ID): P
357
369
  sessionStorage.removeItem(`${SESSION_PREFIX}${id}:version`);
358
370
  }
359
371
 
360
- async function sessionStoreShareVersion(version: number, id: string = DEFAULT_DEVICE_SHARE_ID): Promise<void> {
372
+ async function sessionStoreShareVersion(
373
+ version: number,
374
+ id: string = DEFAULT_DEVICE_SHARE_ID
375
+ ): Promise<void> {
361
376
  sessionStorage.setItem(`${SESSION_PREFIX}${id}:version`, String(version));
362
377
  }
363
378
 
364
- async function sessionGetShareVersion(id: string = DEFAULT_DEVICE_SHARE_ID): Promise<number | null> {
379
+ async function sessionGetShareVersion(
380
+ id: string = DEFAULT_DEVICE_SHARE_ID
381
+ ): Promise<number | null> {
365
382
  const raw = sessionStorage.getItem(`${SESSION_PREFIX}${id}:version`);
366
383
 
367
384
  return raw !== null ? Number(raw) : null;
@@ -400,19 +417,16 @@ export function createAdaptiveStorage() {
400
417
  : storeDeviceShare(share, id),
401
418
 
402
419
  getDeviceShare: (id?: string) =>
403
- isPublicComputerMode()
404
- ? sessionGetDeviceShare(id)
405
- : getDeviceShare(id),
420
+ isPublicComputerMode() ? sessionGetDeviceShare(id) : getDeviceShare(id),
406
421
 
407
422
  hasDeviceShare: (id?: string) =>
408
- isPublicComputerMode()
409
- ? sessionHasDeviceShare(id)
410
- : hasDeviceShare(id),
423
+ isPublicComputerMode() ? sessionHasDeviceShare(id) : hasDeviceShare(id),
411
424
 
412
425
  clearAllShares: (id?: string) =>
413
- isPublicComputerMode()
414
- ? sessionClearAllShares(id)
415
- : clearAllShares(id),
426
+ isPublicComputerMode() ? sessionClearAllShares(id) : clearAllShares(id),
427
+
428
+ deleteDeviceShare: (id?: string) =>
429
+ isPublicComputerMode() ? sessionDeleteDeviceShare(id) : deleteDeviceShare(id),
416
430
 
417
431
  storeShareVersion: (version: number, id?: string) =>
418
432
  isPublicComputerMode()
@@ -420,9 +434,7 @@ export function createAdaptiveStorage() {
420
434
  : storeShareVersion(version, id),
421
435
 
422
436
  getShareVersion: (id?: string) =>
423
- isPublicComputerMode()
424
- ? sessionGetShareVersion(id)
425
- : getShareVersion(id),
437
+ isPublicComputerMode() ? sessionGetShareVersion(id) : getShareVersion(id),
426
438
  };
427
439
  }
428
440
 
package/src/types.ts CHANGED
@@ -16,11 +16,23 @@ export {
16
16
  type AuthProvider,
17
17
  type RecoveryMethodInfo,
18
18
  type RecoveryResult,
19
+ type IdentityRecoverySession,
19
20
  type ServerKeyStatus,
21
+ type SssActivationState,
20
22
  type KeyDerivationStrategy,
23
+ type DidAuthVpSigner,
24
+ type EscrowEnrollmentOptions,
25
+ type EscrowEnrollmentState,
26
+ type EscrowPinStatus,
21
27
  } from '@learncard/types';
22
28
 
23
- import type { AuthProvider, AuthProviderType, KeyDerivationStrategy, RecoveryMethodInfo } from '@learncard/types';
29
+ import type {
30
+ AuthProvider,
31
+ AuthProviderType,
32
+ KeyDerivationStrategy,
33
+ RecoveryMethodInfo,
34
+ SssActivationState,
35
+ } from '@learncard/types';
24
36
 
25
37
  // ---------------------------------------------------------------------------
26
38
  // SSS-specific: Contact & auth provider mapping
@@ -54,7 +66,70 @@ export const SecurityLevels: readonly SecurityLevel[] = ['basic', 'enhanced', 'a
54
66
  * SSS recovery method type identifiers.
55
67
  * These are the specific recovery methods supported by the SSS strategy.
56
68
  */
57
- export type RecoveryMethodType = 'passkey' | 'backup' | 'phrase' | 'email';
69
+ export type RecoveryMethodType = 'passkey' | 'backup' | 'phrase' | 'email' | 'escrow';
70
+
71
+ /** Trust policy for the enclave that receives recovery material. */
72
+ export type EscrowAttestationPolicy =
73
+ | { mode: 'software'; pinnedPublicKeys: string[] }
74
+ | { mode: 'nitro'; pinnedMeasurements: { imageSha384: string }[]; rootCertificatePem?: string };
75
+
76
+ /** Public status of an escrow recovery waiting period. */
77
+ export interface EscrowHoldStatus {
78
+ holdId: string;
79
+ releasePolicy: 'hold' | 'pin';
80
+ status: 'pending' | 'cancelled' | 'completed' | 'expired';
81
+ requestedAt: string;
82
+ releaseAfter: string;
83
+ cancelledAt?: string;
84
+ completedAt?: string;
85
+ }
86
+
87
+ /** Persist these secrets securely until completion; a null token means an existing hold. */
88
+ export type EscrowRecoveryStart = EscrowHoldStatus & {
89
+ resumeToken: string | null;
90
+ clientEphemeralPrivateKey: string;
91
+ pinSalt?: string;
92
+ };
93
+
94
+ /** The account has no available PIN release policy. */
95
+ export class EscrowPinUnavailableError extends Error {
96
+ constructor() {
97
+ super('PIN recovery is not available for this account.');
98
+ this.name = 'EscrowPinUnavailableError';
99
+ }
100
+ }
101
+
102
+ /** A failed PIN attempt consumes its hold; another attempt must start a fresh hold. */
103
+ export class EscrowPinMismatchError extends Error {
104
+ constructor(public readonly attemptsRemaining: number) {
105
+ super('Incorrect PIN.');
106
+ this.name = 'EscrowPinMismatchError';
107
+ }
108
+ }
109
+
110
+ /** Temporary throttle: retry after cooldown with a fresh hold, never retry /complete. */
111
+ export class EscrowPinThrottledError extends Error {
112
+ constructor() {
113
+ super('Too many tries right now. Wait a minute and try again.');
114
+ this.name = 'EscrowPinThrottledError';
115
+ }
116
+ }
117
+
118
+ /** A pending waiting-period request is too recent to restart. */
119
+ export class EscrowHoldRestartThrottledError extends Error {
120
+ constructor(public readonly retryAfter?: string) {
121
+ super('A recovery request was started recently. Please wait before restarting.');
122
+ this.name = 'EscrowHoldRestartThrottledError';
123
+ }
124
+ }
125
+
126
+ /** PIN recovery is exhausted; delayed escrow recovery remains available. */
127
+ export class EscrowPinLockedError extends Error {
128
+ constructor() {
129
+ super('Too many incorrect PIN attempts. You can still recover by waiting.');
130
+ this.name = 'EscrowPinLockedError';
131
+ }
132
+ }
58
133
 
59
134
  export interface PasskeyRecoveryMethod {
60
135
  type: 'passkey';
@@ -73,12 +148,15 @@ export interface RecoveryPhraseRecoveryMethod {
73
148
  }
74
149
 
75
150
  /** @deprecated Use RecoveryInput instead. Kept for legacy SSSKeyManager class. */
76
- export type RecoveryMethod = PasskeyRecoveryMethod | BackupFileRecoveryMethod | RecoveryPhraseRecoveryMethod;
151
+ export type RecoveryMethod =
152
+ PasskeyRecoveryMethod | BackupFileRecoveryMethod | RecoveryPhraseRecoveryMethod;
77
153
 
78
154
  /**
79
155
  * SSS-specific recovery input — what the user provides to recover their key.
80
156
  */
81
157
  export type RecoveryInput =
158
+ | { method: 'escrow-pin'; pin: string }
159
+ | { method: 'escrow'; holdId: string; resumeToken: string; clientEphemeralPrivateKey: string }
82
160
  | { method: 'passkey'; credentialId: string }
83
161
  | { method: 'phrase'; phrase: string }
84
162
  | { method: 'backup'; fileContents: string; password: string }
@@ -88,20 +166,34 @@ export type RecoveryInput =
88
166
  * SSS-specific recovery setup input — what the user provides to set up a method.
89
167
  */
90
168
  export type RecoverySetupInput =
169
+ | { method: 'escrow'; pin?: string }
91
170
  | { method: 'passkey' }
92
171
  | { method: 'phrase' }
93
172
  | { method: 'backup'; password: string; did: string }
94
- | { method: 'email' };
173
+ | { method: 'email'; email: string };
95
174
 
96
175
  /**
97
176
  * SSS-specific recovery setup result.
98
177
  */
99
178
  export type RecoverySetupResult =
179
+ | { method: 'escrow'; shareVersion: number }
100
180
  | { method: 'passkey'; credentialId: string }
101
- | { method: 'phrase'; phrase: string }
181
+ | {
182
+ method: 'phrase';
183
+ phrase: string;
184
+ challengeWordIndices: number[];
185
+ /** Shuffled choices per challenged word (the answer plus decoys not in the phrase). */
186
+ challengeWordOptions: string[][];
187
+ }
102
188
  | { method: 'backup'; backupFile: BackupFile }
103
189
  | { method: 'email' };
104
190
 
191
+ /** Proof supplied after a pending recovery method has been created. */
192
+ export type RecoveryConfirmationInput =
193
+ | { method: 'phrase'; challengeWords: string[] }
194
+ | { method: 'backup'; fileContents: string; password: string }
195
+ | { method: 'email'; code: string };
196
+
105
197
  // ---------------------------------------------------------------------------
106
198
  // SSS-specific: Share & encryption types
107
199
  // ---------------------------------------------------------------------------
@@ -133,6 +225,8 @@ export interface UserKeyRecord {
133
225
  recoveryMethods: RecoveryMethodInfo[];
134
226
  migratedFromWeb3Auth: boolean;
135
227
  migratedAt?: Date;
228
+ sssActivationState?: SssActivationState;
229
+ provisionalCreatedAt?: Date;
136
230
  createdAt: Date;
137
231
  updatedAt: Date;
138
232
  }
@@ -142,6 +236,8 @@ export interface BackupFile {
142
236
  createdAt: string;
143
237
  primaryDid: string;
144
238
  shareVersion?: number;
239
+ /** SHA-256 checksum of the decrypted share, used to verify a just-written file. */
240
+ shareChecksum?: string;
145
241
  encryptedShare: {
146
242
  ciphertext: string;
147
243
  iv: string;
@@ -173,7 +269,12 @@ export interface SSSKeyManagerConfig {
173
269
  * The SSS key derivation strategy — a KeyDerivationStrategy narrowed
174
270
  * with SSS-specific recovery input/output types.
175
271
  */
176
- export type SSSKeyDerivationStrategy = KeyDerivationStrategy<RecoveryInput, RecoverySetupInput, RecoverySetupResult>;
272
+ export type SSSKeyDerivationStrategy = KeyDerivationStrategy<
273
+ RecoveryInput,
274
+ RecoverySetupInput,
275
+ RecoverySetupResult,
276
+ RecoveryConfirmationInput
277
+ >;
177
278
 
178
279
  // ---------------------------------------------------------------------------
179
280
  // Legacy types (deprecated)