Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
611556b
feat(seedless-onboarding): add dataType support for secret data items
huggingbot Dec 2, 2025
85275f5
refactor: Removed default dataType parameter from createToprfKeyAndBa…
huggingbot Dec 2, 2025
fc73107
refactor: update documentation for secret data item methods
huggingbot Dec 2, 2025
3afa879
refactor: enhance primary secret data validation in SeedlessOnboardin…
huggingbot Dec 2, 2025
90d642b
fix: Update sorting mechanism to prioritize PrimarySrp dataType over …
huggingbot Dec 2, 2025
ac52a76
feat(seedless-onboarding-controller): add createdAt field and sort by…
huggingbot Dec 3, 2025
11c09f8
feat(seedless-onboarding-controller): add storage metadata to SecretM…
huggingbot Dec 3, 2025
2885314
fix(seedless-onboarding-controller): use timestamp extraction for TIM…
huggingbot Dec 5, 2025
3e2b477
chore: Update version
huggingbot Dec 16, 2025
f0b9638
Merge branch 'main' into feat/data-type
huggingbot Dec 16, 2025
ebcc4e0
chore: Update CHANGELOG and remove deprecated methods in SecretMetadata
huggingbot Dec 16, 2025
b279fa5
fix: improve sorting logic for secret data with mixed createdAt values
huggingbot Dec 18, 2025
074d8af
feat: add runMigrations for legacy dataType migration
huggingbot Jan 5, 2026
eb77007
feat: add setMigrationVersion method for direct migration version set…
huggingbot Jan 5, 2026
0bde969
Merge branch 'main' into feat/data-type
huggingbot Jan 5, 2026
7634383
feat: update SecretMetadata to use SecretDataItemOutput for storageVe…
huggingbot Jan 6, 2026
12d0ed0
fix: update SecretMetadata method signature to require storageMetadat…
huggingbot Jan 6, 2026
3aae970
refactor: remove SecretMetadataVersion from SecretMetadata and relate…
huggingbot Jan 6, 2026
e820ef5
fix: handle null dataType in SeedlessOnboardingController validation
huggingbot Jan 6, 2026
22b695e
refactor: replace type param with dataType in addNewSecretData
huggingbot Jan 6, 2026
c0dcad7
refactor: update SecretMetadata to streamline constructor options and…
huggingbot Jan 6, 2026
6e5a3ce
fix: ensure password synchronization before running migrations in See…
huggingbot Jan 6, 2026
ed01779
fix: handle null dataType in SecretMetadata constructor options
huggingbot Jan 6, 2026
10c0c09
refactor: remove updateSecretDataItem and batchUpdateSecretDataItems …
huggingbot Jan 6, 2026
2f02601
refactor: rename SeedlessOnboardingMigrationVersion.DataType to Seedl…
huggingbot Jan 6, 2026
4019328
refactor: move secret metadata sorting logic to SecretMetadata.compare
huggingbot Jan 6, 2026
2f3255a
Merge branch 'main' into feat/data-type
huggingbot Jan 6, 2026
b78c9af
chore: Update changelog
huggingbot Jan 6, 2026
5c95f56
chore: update @metamask/toprf-secure-backup to version 1.0.0 in packa…
huggingbot Jan 15, 2026
e0e6499
fix: preserve PrimarySrp designation during migrations
huggingbot Jan 15, 2026
7184599
fix: filter skipped items in hasPrimarySrp check
huggingbot Jan 15, 2026
614be60
fix: prevent addition of PrimarySrp dataType in addNewSecretData method
huggingbot Jan 20, 2026
c1e534c
feat: set migration version to V1 upon creating new backup format
huggingbot Jan 20, 2026
483d0cd
fix: Use error constant
huggingbot Jan 21, 2026
b59af15
Merge branch 'main' into feat/data-type
huggingbot Jan 26, 2026
6ae8a59
refactor: update runMigrations to return boolean status
huggingbot Jan 26, 2026
8d17f38
Merge branch 'main' into feat/data-type
huggingbot Feb 23, 2026
7a5e77f
Merge branch 'main' into feat/data-type
huggingbot Feb 23, 2026
7141349
fix: Lint
huggingbot Feb 23, 2026
5d62ac6
Merge branch 'main' into feat/data-type
huggingbot Mar 16, 2026
6ba2848
Merge branch 'main' into feat/data-type
huggingbot Mar 24, 2026
93f8bb3
chore: Lint
huggingbot Mar 24, 2026
ab4fa16
Merge branch 'main' into feat/data-type
huggingbot Apr 7, 2026
d7cd996
chore: Update Changelog
huggingbot Apr 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/seedless-onboarding-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add optional `dataType` parameter to `addNewSecretData` method for categorizing secret data on insert
- Add `updateSecretDataItem` method to update fields for existing items by `itemId`
- Add `batchUpdateSecretDataItems` method to batch update fields for multiple items
- Add `itemId`, `dataType`, and `createdAt` storage-level properties to `SecretMetadata`
- Add `SecretMetadata.compareByTimestamp` static method for comparing metadata by timestamp
- Add `SecretMetadata.matchesType` static method for checking if metadata matches a given type
- Re-export `EncAccountDataType` from `@metamask/toprf-secure-backup`

### Changed

- **BREAKING:** Remove `parseSecretsFromMetadataStore`, `fromBatch`, and `sort` methods from `SecretMetadata`
- Use `SecretMetadata.compareByTimestamp` for sorting
- Use `SecretMetadata.matchesType` for filtering
- Bump `@metamask/toprf-secure-backup` from `^0.10.0` to `^0.11.0`
- Move peer dependencies for controller and service packages to direct dependencies ([#7209](https://github.com/MetaMask/core/pull/7209))
- The dependencies moved are:
- `@metamask/keyring-controller` (^25.0.0)
Expand Down
2 changes: 1 addition & 1 deletion packages/seedless-onboarding-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@metamask/browser-passworder": "^6.0.0",
"@metamask/keyring-controller": "^25.0.0",
"@metamask/messenger": "^0.3.0",
"@metamask/toprf-secure-backup": "^0.10.0",
"@metamask/toprf-secure-backup": "^0.11.0",
"@metamask/utils": "^11.8.1",
"@noble/ciphers": "^1.3.0",
"@noble/curves": "^1.9.2",
Expand Down
155 changes: 76 additions & 79 deletions packages/seedless-onboarding-controller/src/SecretMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EncAccountDataType } from '@metamask/toprf-secure-backup';
import {
base64ToBytes,
bytesToBase64,
Expand Down Expand Up @@ -42,6 +43,15 @@ type SecretMetadataJson<DataType extends SecretDataType> = Omit<
* const secretMetadata = new SecretMetadata(secret);
* ```
*/
/**
* Storage-level metadata from the metadata store (not encrypted).
*/
type StorageMetadata = {
itemId?: string;
dataType?: EncAccountDataType;
createdAt?: string;
};

export class SecretMetadata<DataType extends SecretDataType = Uint8Array>
implements ISecretMetadata<DataType>
{
Expand All @@ -53,50 +63,35 @@ export class SecretMetadata<DataType extends SecretDataType = Uint8Array>

readonly #version: SecretMetadataVersion;

// Storage-level metadata (not encrypted)
readonly #itemId?: string;

readonly #dataType?: EncAccountDataType;

readonly #createdAt?: string;

/**
* Create a new SecretMetadata instance.
*
* @param data - The secret to add metadata to.
* @param options - The options for the secret metadata.
* @param options.timestamp - The timestamp when the secret was created.
* @param options.type - The type of the secret.
* @param options.version - The version of the secret metadata.
* @param storageMetadata - Storage-level metadata from the metadata store.
*/
constructor(data: DataType, options?: Partial<SecretMetadataOptions>) {
constructor(
data: DataType,
options?: Partial<SecretMetadataOptions>,
Comment thread
huggingbot marked this conversation as resolved.
Outdated
storageMetadata?: StorageMetadata,
) {
this.#data = data;
this.#timestamp = options?.timestamp ?? Date.now();
this.#type = options?.type ?? SecretType.Mnemonic;
this.#version = options?.version ?? SecretMetadataVersion.V1;
Comment thread
huggingbot marked this conversation as resolved.
Outdated
}

/**
* Create an Array of SecretMetadata instances from an array of secrets.
*
* To respect the order of the secrets, we add the index to the timestamp
* so that the first secret backup will have the oldest timestamp
* and the last secret backup will have the newest timestamp.
*
* @param data - The data to add metadata to.
* @param data.value - The SeedPhrase/PrivateKey to add metadata to.
* @param data.options - The options for the seed phrase metadata.
* @returns The SecretMetadata instances.
*/
static fromBatch<DataType extends SecretDataType = Uint8Array>(
data: {
value: DataType;
options?: Partial<SecretMetadataOptions>;
}[],
): SecretMetadata<DataType>[] {
const timestamp = Date.now();
return data.map((d, index) => {
// To respect the order of the seed phrases, we add the index to the timestamp
// so that the first seed phrase backup will have the oldest timestamp
// and the last seed phrase backup will have the newest timestamp
const backupCreatedAt = d.options?.timestamp ?? timestamp + index * 5;
return new SecretMetadata(d.value, {
timestamp: backupCreatedAt,
type: d.options?.type,
});
});
this.#itemId = storageMetadata?.itemId;
this.#dataType = storageMetadata?.dataType;
this.#createdAt = storageMetadata?.createdAt;
}

/**
Expand All @@ -122,42 +117,16 @@ export class SecretMetadata<DataType extends SecretDataType = Uint8Array>
}
}

/**
* Parse the SecretMetadata from the metadata store and return the array of SecretMetadata instances.
*
* This method also sorts the secrets by timestamp in ascending order, i.e. the oldest secret will be the first element in the array.
*
* @param secretMetadataArr - The array of SecretMetadata from the metadata store.
* @param filterType - The type of the secret to filter.
* @returns The array of SecretMetadata instances.
*/
static parseSecretsFromMetadataStore<
DataType extends SecretDataType = Uint8Array,
>(
secretMetadataArr: Uint8Array[],
filterType?: SecretType,
): SecretMetadata<DataType>[] {
const parsedSecertMetadata = secretMetadataArr.map((metadata) =>
SecretMetadata.fromRawMetadata<DataType>(metadata),
);

const secrets = SecretMetadata.sort(parsedSecertMetadata);

if (filterType) {
return secrets.filter((secret) => secret.type === filterType);
}

return secrets;
}

/**
* Parse and create the SecretMetadata instance from the raw metadata bytes.
*
* @param rawMetadata - The raw metadata.
* @param storageMetadata - Storage-level metadata from the metadata store.
* @returns The parsed secret metadata.
*/
static fromRawMetadata<DataType extends SecretDataType>(
rawMetadata: Uint8Array,
storageMetadata?: StorageMetadata,
Comment thread
huggingbot marked this conversation as resolved.
Outdated
): SecretMetadata<DataType> {
const serializedMetadata = bytesToString(rawMetadata);
const parsedMetadata = JSON.parse(serializedMetadata);
Comment thread
huggingbot marked this conversation as resolved.
Expand All @@ -175,31 +144,47 @@ export class SecretMetadata<DataType extends SecretDataType = Uint8Array>
data = parsedMetadata.data as DataType;
}

return new SecretMetadata<DataType>(data, {
timestamp: parsedMetadata.timestamp,
type,
version,
});
return new SecretMetadata<DataType>(
data,
{
Comment thread
huggingbot marked this conversation as resolved.
Outdated
timestamp: parsedMetadata.timestamp,
type,
version,
},
storageMetadata,
);
}

/**
* Sort the seed phrases by timestamp.
*
* @param data - The secret metadata array to sort.
* @param order - The order to sort the seed phrases. Default is `desc`.
* Compare two SecretMetadata instances by timestamp.
*
* @returns The sorted secret metadata array.
* @param a - The first SecretMetadata instance.
* @param b - The second SecretMetadata instance.
* @param order - The sort order. Default is 'asc'.
* @returns A negative number if a < b, positive if a > b, zero if equal.
*/
static sort<DataType extends SecretDataType = Uint8Array>(
data: SecretMetadata<DataType>[],
static compareByTimestamp<DataType extends SecretDataType = SecretDataType>(
a: SecretMetadata<DataType>,
b: SecretMetadata<DataType>,
order: 'asc' | 'desc' = 'asc',
): SecretMetadata<DataType>[] {
return data.sort((a, b) => {
if (order === 'asc') {
return a.timestamp - b.timestamp;
}
return b.timestamp - a.timestamp;
});
): number {
return order === 'asc'
? a.timestamp - b.timestamp
Comment thread
huggingbot marked this conversation as resolved.
: b.timestamp - a.timestamp;
}

/**
* Check if a SecretMetadata instance matches the given type.
*
* @param secret - The SecretMetadata instance to check.
* @param type - The type to match against.
* @returns True if the secret matches the type.
*/
static matchesType<DataType extends SecretDataType = SecretDataType>(
secret: SecretMetadata<DataType>,
type: SecretType,
): boolean {
return secret.type === type;
}

get data(): DataType {
Expand All @@ -218,6 +203,18 @@ export class SecretMetadata<DataType extends SecretDataType = Uint8Array>
return this.#version;
}

get itemId() {
return this.#itemId;
}

get dataType() {
return this.#dataType;
}

get createdAt() {
return this.#createdAt;
}

/**
* Serialize the secret metadata and convert it to a Uint8Array.
*
Expand Down
Loading