@metamask-previews/mfa-recovery-controller 0.0.0-preview-a2f6118d2

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 (46) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +6 -0
  3. package/LICENSE.APACHE2 +201 -0
  4. package/LICENSE.MIT +21 -0
  5. package/README.md +22 -0
  6. package/dist/MfaRecoveryController-method-action-types.d.ts +88 -0
  7. package/dist/MfaRecoveryController-method-action-types.d.ts.map +1 -0
  8. package/dist/MfaRecoveryController-method-action-types.js +6 -0
  9. package/dist/MfaRecoveryController-method-action-types.js.map +1 -0
  10. package/dist/MfaRecoveryController.d.ts +119 -0
  11. package/dist/MfaRecoveryController.d.ts.map +1 -0
  12. package/dist/MfaRecoveryController.js +456 -0
  13. package/dist/MfaRecoveryController.js.map +1 -0
  14. package/dist/crypto.d.ts +111 -0
  15. package/dist/crypto.d.ts.map +1 -0
  16. package/dist/crypto.js +268 -0
  17. package/dist/crypto.js.map +1 -0
  18. package/dist/errors.d.ts +16 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +25 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/escrow-utils.d.ts +42 -0
  23. package/dist/escrow-utils.d.ts.map +1 -0
  24. package/dist/escrow-utils.js +77 -0
  25. package/dist/escrow-utils.js.map +1 -0
  26. package/dist/identifier-auth.d.ts +83 -0
  27. package/dist/identifier-auth.d.ts.map +1 -0
  28. package/dist/identifier-auth.js +119 -0
  29. package/dist/identifier-auth.js.map +1 -0
  30. package/dist/index.d.ts +7 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +4 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/pending-operation-validation.d.ts +21 -0
  35. package/dist/pending-operation-validation.d.ts.map +1 -0
  36. package/dist/pending-operation-validation.js +181 -0
  37. package/dist/pending-operation-validation.js.map +1 -0
  38. package/dist/state-machine.d.ts +23 -0
  39. package/dist/state-machine.d.ts.map +1 -0
  40. package/dist/state-machine.js +34 -0
  41. package/dist/state-machine.js.map +1 -0
  42. package/dist/types.d.ts +235 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +5 -0
  45. package/dist/types.js.map +1 -0
  46. package/package.json +76 -0
@@ -0,0 +1,119 @@
1
+ import { generateSigningKey, hash, sign } from './crypto.js';
2
+ import { MfaRecoveryError } from './errors.js';
3
+ import { isFulfilledResult } from './escrow-utils.js';
4
+ /**
5
+ * Minimum identifier count for register and updateIdentifiers.
6
+ */
7
+ export const MIN_IDENTIFIERS = 2;
8
+ /**
9
+ * Distinct identifier identities (`type` + `namespace` + `value`).
10
+ *
11
+ * @param identifiers - Identifier list.
12
+ * @returns Distinct identity count.
13
+ */
14
+ export function countDistinctIdentifiers(identifiers) {
15
+ return new Set(identifiers.map(({ type, namespace, value }) => `${type}\0${namespace}\0${value}`)).size;
16
+ }
17
+ /**
18
+ * Trusted identifier-type registry. Escrows and the controller derive the
19
+ * authentication mode from this table, never from a client-selected flag.
20
+ *
21
+ * TODO: MFA-605 / MFA-606 / MFA-568 — add `emailOtp` / `smsOtp` as
22
+ * `escrow-challenge` once escrow OTP commands exist.
23
+ */
24
+ export const IDENTIFIER_AUTH_MODES = {
25
+ oidc: 'key-bound',
26
+ passkey: 'key-bound',
27
+ siwe: 'key-bound',
28
+ };
29
+ /**
30
+ * Requests a key-bound identifier token and returns the private key used to
31
+ * prove possession of it.
32
+ *
33
+ * @param params - Identifier authorization parameters.
34
+ * @param params.identifier - Identifier to authenticate.
35
+ * @param params.requestHash - Request hash bound to the authorization.
36
+ * @param params.identifierAuthProvider - Provider for key-bound assertions.
37
+ * @returns Key-bound identifier token and its proof private key.
38
+ */
39
+ export async function requestKeyBoundIdentifierToken({ identifier, requestHash, identifierAuthProvider, }) {
40
+ const proofKey = generateSigningKey();
41
+ const token = await identifierAuthProvider.getKeyBoundIdentifierToken({
42
+ identifier,
43
+ proofPublicKey: proofKey.publicKey,
44
+ requestHash,
45
+ });
46
+ return { token, proofPrivateKey: proofKey.privateKey };
47
+ }
48
+ /**
49
+ * Authorizes a key-bound identifier with each escrow that successfully
50
+ * provides a challenge.
51
+ *
52
+ * @param params - Key-bound authorization parameters.
53
+ * @param params.escrows - Escrows to authorize.
54
+ * @param params.token - Key-bound identifier token.
55
+ * @param params.proofPrivateKey - Private key for proving token possession.
56
+ * @returns Successful escrow authorizations.
57
+ */
58
+ export async function authorizeWithKeyBoundToken({ escrows, token, proofPrivateKey, }) {
59
+ const challengeResults = await Promise.allSettled(escrows.map((escrow) => escrow.generateChallenge()));
60
+ const challenges = challengeResults.flatMap((result, index) => isFulfilledResult(result)
61
+ ? [{ escrow: escrows[index], challenge: result.value }]
62
+ : []);
63
+ const authorizationResults = await Promise.allSettled(challenges.map(async ({ escrow, challenge }) => {
64
+ const message = hash([token, challenge.id, token.requestHash]);
65
+ return {
66
+ escrow,
67
+ authorization: {
68
+ kind: 'key-bound',
69
+ token,
70
+ proof: {
71
+ challengeId: challenge.id,
72
+ requestHash: token.requestHash,
73
+ signature: sign(proofPrivateKey, message),
74
+ },
75
+ },
76
+ };
77
+ }));
78
+ return authorizationResults
79
+ .filter(isFulfilledResult)
80
+ .map((result) => result.value);
81
+ }
82
+ /**
83
+ * Requests and authorizes a key-bound identifier with each escrow that
84
+ * successfully provides a challenge.
85
+ *
86
+ * @param params - Identifier authorization parameters.
87
+ * @param params.escrows - Escrows to authorize.
88
+ * @param params.identifier - Identifier to authorize.
89
+ * @param params.requestHash - Request hash bound to the authorization.
90
+ * @param params.identifierAuthProvider - Provider for key-bound assertions.
91
+ * @returns Successful escrow authorizations.
92
+ */
93
+ export async function authorizeKeyBoundIdentifier({ escrows, identifier, requestHash, identifierAuthProvider, }) {
94
+ const { token, proofPrivateKey } = await requestKeyBoundIdentifierToken({
95
+ identifier,
96
+ requestHash,
97
+ identifierAuthProvider,
98
+ });
99
+ return await authorizeWithKeyBoundToken({
100
+ escrows,
101
+ token,
102
+ proofPrivateKey,
103
+ });
104
+ }
105
+ /**
106
+ * Returns the auth mode for an identifier type.
107
+ *
108
+ * @param type - Identifier type.
109
+ * @returns Auth mode.
110
+ * @throws If the type is not in the trusted registry.
111
+ */
112
+ export function getIdentifierAuthMode(type) {
113
+ const mode = IDENTIFIER_AUTH_MODES[type];
114
+ if (mode === undefined) {
115
+ throw new MfaRecoveryError(`Unknown identifier type: ${type}`, 'unknown_identifier_type');
116
+ }
117
+ return mode;
118
+ }
119
+ //# sourceMappingURL=identifier-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identifier-auth.js","sourceRoot":"","sources":["../src/identifier-auth.ts"],"names":[],"mappings":"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","sourcesContent":["import { generateSigningKey, hash, sign } from './crypto.js';\nimport { MfaRecoveryError } from './errors.js';\nimport { isFulfilledResult } from './escrow-utils.js';\nimport type {\n Identifier,\n IdentifierAuthMode,\n IdentifierAuthorization,\n KeyBoundIdentifierToken,\n RecoveryEscrowProvider,\n RecoveryIdentifierAuthProvider,\n} from './types.js';\n\n/**\n * Minimum identifier count for register and updateIdentifiers.\n */\nexport const MIN_IDENTIFIERS = 2;\n\n/**\n * Distinct identifier identities (`type` + `namespace` + `value`).\n *\n * @param identifiers - Identifier list.\n * @returns Distinct identity count.\n */\nexport function countDistinctIdentifiers(identifiers: Identifier[]): number {\n return new Set(\n identifiers.map(\n ({ type, namespace, value }) => `${type}\\0${namespace}\\0${value}`,\n ),\n ).size;\n}\n\n/**\n * Trusted identifier-type registry. Escrows and the controller derive the\n * authentication mode from this table, never from a client-selected flag.\n *\n * TODO: MFA-605 / MFA-606 / MFA-568 — add `emailOtp` / `smsOtp` as\n * `escrow-challenge` once escrow OTP commands exist.\n */\nexport const IDENTIFIER_AUTH_MODES: Record<string, IdentifierAuthMode> = {\n oidc: 'key-bound',\n passkey: 'key-bound',\n siwe: 'key-bound',\n};\n\nexport type AuthorizedEscrow = {\n escrow: RecoveryEscrowProvider;\n authorization: IdentifierAuthorization;\n};\n\n/**\n * Requests a key-bound identifier token and returns the private key used to\n * prove possession of it.\n *\n * @param params - Identifier authorization parameters.\n * @param params.identifier - Identifier to authenticate.\n * @param params.requestHash - Request hash bound to the authorization.\n * @param params.identifierAuthProvider - Provider for key-bound assertions.\n * @returns Key-bound identifier token and its proof private key.\n */\nexport async function requestKeyBoundIdentifierToken({\n identifier,\n requestHash,\n identifierAuthProvider,\n}: {\n identifier: Identifier;\n requestHash: string;\n identifierAuthProvider: RecoveryIdentifierAuthProvider;\n}): Promise<{ token: KeyBoundIdentifierToken; proofPrivateKey: string }> {\n const proofKey = generateSigningKey();\n const token = await identifierAuthProvider.getKeyBoundIdentifierToken({\n identifier,\n proofPublicKey: proofKey.publicKey,\n requestHash,\n });\n return { token, proofPrivateKey: proofKey.privateKey };\n}\n\n/**\n * Authorizes a key-bound identifier with each escrow that successfully\n * provides a challenge.\n *\n * @param params - Key-bound authorization parameters.\n * @param params.escrows - Escrows to authorize.\n * @param params.token - Key-bound identifier token.\n * @param params.proofPrivateKey - Private key for proving token possession.\n * @returns Successful escrow authorizations.\n */\nexport async function authorizeWithKeyBoundToken({\n escrows,\n token,\n proofPrivateKey,\n}: {\n escrows: RecoveryEscrowProvider[];\n token: KeyBoundIdentifierToken;\n proofPrivateKey: string;\n}): Promise<AuthorizedEscrow[]> {\n const challengeResults = await Promise.allSettled(\n escrows.map((escrow) => escrow.generateChallenge()),\n );\n const challenges = challengeResults.flatMap((result, index) =>\n isFulfilledResult(result)\n ? [{ escrow: escrows[index], challenge: result.value }]\n : [],\n );\n const authorizationResults = await Promise.allSettled(\n challenges.map(async ({ escrow, challenge }) => {\n const message = hash([token, challenge.id, token.requestHash]);\n return {\n escrow,\n authorization: {\n kind: 'key-bound' as const,\n token,\n proof: {\n challengeId: challenge.id,\n requestHash: token.requestHash,\n signature: sign(proofPrivateKey, message),\n },\n },\n };\n }),\n );\n return authorizationResults\n .filter(isFulfilledResult)\n .map((result) => result.value);\n}\n\n/**\n * Requests and authorizes a key-bound identifier with each escrow that\n * successfully provides a challenge.\n *\n * @param params - Identifier authorization parameters.\n * @param params.escrows - Escrows to authorize.\n * @param params.identifier - Identifier to authorize.\n * @param params.requestHash - Request hash bound to the authorization.\n * @param params.identifierAuthProvider - Provider for key-bound assertions.\n * @returns Successful escrow authorizations.\n */\nexport async function authorizeKeyBoundIdentifier({\n escrows,\n identifier,\n requestHash,\n identifierAuthProvider,\n}: {\n escrows: RecoveryEscrowProvider[];\n identifier: Identifier;\n requestHash: string;\n identifierAuthProvider: RecoveryIdentifierAuthProvider;\n}): Promise<AuthorizedEscrow[]> {\n const { token, proofPrivateKey } = await requestKeyBoundIdentifierToken({\n identifier,\n requestHash,\n identifierAuthProvider,\n });\n return await authorizeWithKeyBoundToken({\n escrows,\n token,\n proofPrivateKey,\n });\n}\n\n/**\n * Returns the auth mode for an identifier type.\n *\n * @param type - Identifier type.\n * @returns Auth mode.\n * @throws If the type is not in the trusted registry.\n */\nexport function getIdentifierAuthMode(type: string): IdentifierAuthMode {\n const mode = IDENTIFIER_AUTH_MODES[type];\n if (mode === undefined) {\n throw new MfaRecoveryError(\n `Unknown identifier type: ${type}`,\n 'unknown_identifier_type',\n );\n }\n return mode;\n}\n"]}
@@ -0,0 +1,7 @@
1
+ export { MfaRecoveryController, getDefaultMfaRecoveryControllerState, } from './MfaRecoveryController.js';
2
+ export type { MfaRecoveryControllerActions, MfaRecoveryControllerEvents, MfaRecoveryControllerGetStateAction, MfaRecoveryControllerMessenger, MfaRecoveryControllerOptions, MfaRecoveryControllerState, MfaRecoveryControllerStateChangedEvent, } from './MfaRecoveryController.js';
3
+ export type { MfaRecoveryControllerAbortAction, MfaRecoveryControllerAuthenticateIdentifierAction, MfaRecoveryControllerGetPhaseAction, MfaRecoveryControllerGetRecoverySecretAction, MfaRecoveryControllerRegisterAction, MfaRecoveryControllerResumeAction, MfaRecoveryControllerUpdateIdentifiersAction, MfaRecoveryControllerUpdateRecoverySecretAction, } from './MfaRecoveryController-method-action-types.js';
4
+ export { IncompleteMutationError, MfaRecoveryError } from './errors.js';
5
+ export { IDENTIFIER_AUTH_MODES, MIN_IDENTIFIERS, getIdentifierAuthMode, } from './identifier-auth.js';
6
+ export type { AuthControllerToken, EcPublicJwk, EncryptedPendingOperation, Identifier, IdentifierAuthorization, IdentifierSession, Mutation, MutationPayload, MutationReceipt, PendingMutationPayload, PendingOperation, PendingOperationEncryptor, PendingRecoverySecretHex, PendingRegisterPayload, PendingUpdateIdentifiersPayload, PendingUpdateRecoverySecretPayload, RecoveryAuthProvider, RecoveryEscrowProvider, RecoveryIdentifierAuthProvider, RecoveryPhase, RecoveredSecret, WrappedSecret, WrappedSecretOutput, } from './types.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,oCAAoC,GACrC,MAAM,4BAA4B,CAAC;AACpC,YAAY,EACV,4BAA4B,EAC5B,2BAA2B,EAC3B,mCAAmC,EACnC,8BAA8B,EAC9B,4BAA4B,EAC5B,0BAA0B,EAC1B,sCAAsC,GACvC,MAAM,4BAA4B,CAAC;AACpC,YAAY,EACV,gCAAgC,EAChC,iDAAiD,EACjD,mCAAmC,EACnC,4CAA4C,EAC5C,mCAAmC,EACnC,iCAAiC,EACjC,4CAA4C,EAC5C,+CAA+C,GAChD,MAAM,gDAAgD,CAAC;AACxD,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,mBAAmB,EACnB,WAAW,EACX,yBAAyB,EACzB,UAAU,EACV,uBAAuB,EACvB,iBAAiB,EACjB,QAAQ,EACR,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,gBAAgB,EAChB,yBAAyB,EACzB,wBAAwB,EACxB,sBAAsB,EACtB,+BAA+B,EAC/B,kCAAkC,EAClC,oBAAoB,EACpB,sBAAsB,EACtB,8BAA8B,EAC9B,aAAa,EACb,eAAe,EACf,aAAa,EACb,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { MfaRecoveryController, getDefaultMfaRecoveryControllerState, } from './MfaRecoveryController.js';
2
+ export { IncompleteMutationError, MfaRecoveryError } from './errors.js';
3
+ export { IDENTIFIER_AUTH_MODES, MIN_IDENTIFIERS, getIdentifierAuthMode, } from './identifier-auth.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,oCAAoC,GACrC,MAAM,4BAA4B,CAAC;AAoBpC,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,qBAAqB,GACtB,MAAM,sBAAsB,CAAC","sourcesContent":["export {\n MfaRecoveryController,\n getDefaultMfaRecoveryControllerState,\n} from './MfaRecoveryController.js';\nexport type {\n MfaRecoveryControllerActions,\n MfaRecoveryControllerEvents,\n MfaRecoveryControllerGetStateAction,\n MfaRecoveryControllerMessenger,\n MfaRecoveryControllerOptions,\n MfaRecoveryControllerState,\n MfaRecoveryControllerStateChangedEvent,\n} from './MfaRecoveryController.js';\nexport type {\n MfaRecoveryControllerAbortAction,\n MfaRecoveryControllerAuthenticateIdentifierAction,\n MfaRecoveryControllerGetPhaseAction,\n MfaRecoveryControllerGetRecoverySecretAction,\n MfaRecoveryControllerRegisterAction,\n MfaRecoveryControllerResumeAction,\n MfaRecoveryControllerUpdateIdentifiersAction,\n MfaRecoveryControllerUpdateRecoverySecretAction,\n} from './MfaRecoveryController-method-action-types.js';\nexport { IncompleteMutationError, MfaRecoveryError } from './errors.js';\nexport {\n IDENTIFIER_AUTH_MODES,\n MIN_IDENTIFIERS,\n getIdentifierAuthMode,\n} from './identifier-auth.js';\nexport type {\n AuthControllerToken,\n EcPublicJwk,\n EncryptedPendingOperation,\n Identifier,\n IdentifierAuthorization,\n IdentifierSession,\n Mutation,\n MutationPayload,\n MutationReceipt,\n PendingMutationPayload,\n PendingOperation,\n PendingOperationEncryptor,\n PendingRecoverySecretHex,\n PendingRegisterPayload,\n PendingUpdateIdentifiersPayload,\n PendingUpdateRecoverySecretPayload,\n RecoveryAuthProvider,\n RecoveryEscrowProvider,\n RecoveryIdentifierAuthProvider,\n RecoveryPhase,\n RecoveredSecret,\n WrappedSecret,\n WrappedSecretOutput,\n} from './types.js';\n"]}
@@ -0,0 +1,21 @@
1
+ import type { PendingMutationPayload, PendingRegisterPayload, PendingUpdateIdentifiersPayload, PendingUpdateRecoverySecretPayload } from './types.js';
2
+ /**
3
+ * Validates decrypted pending state before it can trigger authorization or
4
+ * escrow operations.
5
+ *
6
+ * @param pending - Decrypted pending state.
7
+ * @param escrowIds - Build-configured escrow ids.
8
+ * @throws If the pending state is malformed or inconsistent.
9
+ */
10
+ export declare function assertValidPendingOperation(pending: unknown, escrowIds: string[]): Promise<void>;
11
+ /**
12
+ * @param payload - Logical pending mutation payload.
13
+ * @returns Whether the payload includes an identifier set.
14
+ */
15
+ export declare function pendingPayloadHasIdentifiers(payload: PendingMutationPayload): payload is PendingRegisterPayload | PendingUpdateIdentifiersPayload;
16
+ /**
17
+ * @param payload - Logical pending mutation payload.
18
+ * @returns Whether the payload includes a hex recovery secret.
19
+ */
20
+ export declare function pendingPayloadHasRecoverySecret(payload: PendingMutationPayload): payload is PendingRegisterPayload | PendingUpdateRecoverySecretPayload;
21
+ //# sourceMappingURL=pending-operation-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-operation-validation.d.ts","sourceRoot":"","sources":["../src/pending-operation-validation.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAKV,sBAAsB,EACtB,sBAAsB,EACtB,+BAA+B,EAC/B,kCAAkC,EACnC,MAAM,YAAY,CAAC;AAEpB;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CA+Ff;AAcD;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,sBAAsB,GAC9B,OAAO,IAAI,sBAAsB,GAAG,+BAA+B,CAErE;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,sBAAsB,GAC9B,OAAO,IAAI,sBAAsB,GAAG,kCAAkC,CAExE"}
@@ -0,0 +1,181 @@
1
+ import { hash } from './crypto.js';
2
+ import { MfaRecoveryError } from './errors.js';
3
+ import { isMutationReceipt } from './escrow-utils.js';
4
+ import { countDistinctIdentifiers, getIdentifierAuthMode, MIN_IDENTIFIERS, } from './identifier-auth.js';
5
+ import { assertSameAudienceIds } from './state-machine.js';
6
+ /**
7
+ * Validates decrypted pending state before it can trigger authorization or
8
+ * escrow operations.
9
+ *
10
+ * @param pending - Decrypted pending state.
11
+ * @param escrowIds - Build-configured escrow ids.
12
+ * @throws If the pending state is malformed or inconsistent.
13
+ */
14
+ export async function assertValidPendingOperation(pending, escrowIds) {
15
+ if (!isRecord(pending)) {
16
+ throwInvalidPendingOperation();
17
+ }
18
+ if (pending.phase !== 'authorizing' && pending.phase !== 'writing') {
19
+ throwInvalidPendingOperation();
20
+ }
21
+ if (!isRecord(pending.mutation)) {
22
+ throwInvalidPendingOperation();
23
+ }
24
+ const mutationValue = pending.mutation;
25
+ if (typeof mutationValue.id !== 'string' ||
26
+ typeof mutationValue.profileId !== 'string' ||
27
+ !isMutationOperation(mutationValue.operation) ||
28
+ typeof mutationValue.expectedVersion !== 'number' ||
29
+ !Number.isInteger(mutationValue.expectedVersion) ||
30
+ mutationValue.expectedVersion < 0 ||
31
+ typeof mutationValue.newVersion !== 'number' ||
32
+ !Number.isInteger(mutationValue.newVersion) ||
33
+ mutationValue.newVersion < 0 ||
34
+ mutationValue.newVersion !== mutationValue.expectedVersion + 1 ||
35
+ typeof mutationValue.payloadHash !== 'string' ||
36
+ typeof mutationValue.requestHash !== 'string' ||
37
+ !Array.isArray(mutationValue.audiences) ||
38
+ !mutationValue.audiences.every((audience) => {
39
+ return typeof audience === 'string';
40
+ })) {
41
+ throwInvalidPendingOperation();
42
+ }
43
+ const mutation = mutationValue;
44
+ assertSameAudienceIds(mutation.audiences, escrowIds);
45
+ const { payload } = pending;
46
+ if (!isPendingPayload(mutation.operation, payload)) {
47
+ throwInvalidPendingOperation();
48
+ }
49
+ if (pendingPayloadHasIdentifiers(payload)) {
50
+ for (const identifier of payload.identifiers) {
51
+ getIdentifierAuthMode(identifier.type);
52
+ }
53
+ }
54
+ if (mutation.operation === 'register'
55
+ ? pending.identifier !== null
56
+ : !isIdentifier(pending.identifier)) {
57
+ throwInvalidPendingOperation();
58
+ }
59
+ if (mutation.operation !== 'register' && isIdentifier(pending.identifier)) {
60
+ getIdentifierAuthMode(pending.identifier.type);
61
+ }
62
+ if (mutation.operation === 'register' && mutation.expectedVersion !== 0) {
63
+ throwInvalidPendingOperation();
64
+ }
65
+ if (hash(payload) !== mutation.payloadHash) {
66
+ throw new MfaRecoveryError('Pending payload does not match mutation', 'payload_mismatch');
67
+ }
68
+ const requestHash = hash({
69
+ id: mutation.id,
70
+ profileId: mutation.profileId,
71
+ operation: mutation.operation,
72
+ expectedVersion: mutation.expectedVersion,
73
+ newVersion: mutation.newVersion,
74
+ payloadHash: mutation.payloadHash,
75
+ audiences: mutation.audiences,
76
+ });
77
+ if (requestHash !== mutation.requestHash) {
78
+ throwInvalidPendingOperation();
79
+ }
80
+ if (pending.phase === 'authorizing') {
81
+ return;
82
+ }
83
+ if (!isAuthControllerToken(pending.authControllerToken) ||
84
+ pending.authControllerToken.profileId !== mutation.profileId ||
85
+ pending.authControllerToken.requestHash !== mutation.requestHash ||
86
+ (mutation.operation !== 'register' &&
87
+ pending.authControllerToken.twoFactor !== true) ||
88
+ ((mutation.operation === 'register' ||
89
+ mutation.operation === 'updateIdentifiers') &&
90
+ (pending.authControllerToken.identifierOwnershipApproved !== true ||
91
+ typeof pending.authControllerToken.identifiersHash !== 'string')) ||
92
+ !isMutationReceiptArray(pending.receipts)) {
93
+ throwInvalidPendingOperation();
94
+ }
95
+ }
96
+ function isRecord(value) {
97
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
98
+ }
99
+ function isMutationOperation(value) {
100
+ return (value === 'register' ||
101
+ value === 'updateRecoverySecret' ||
102
+ value === 'updateIdentifiers');
103
+ }
104
+ /**
105
+ * @param payload - Logical pending mutation payload.
106
+ * @returns Whether the payload includes an identifier set.
107
+ */
108
+ export function pendingPayloadHasIdentifiers(payload) {
109
+ return Object.prototype.hasOwnProperty.call(payload, 'identifiers');
110
+ }
111
+ /**
112
+ * @param payload - Logical pending mutation payload.
113
+ * @returns Whether the payload includes a hex recovery secret.
114
+ */
115
+ export function pendingPayloadHasRecoverySecret(payload) {
116
+ return Object.prototype.hasOwnProperty.call(payload, 'recoverySecret');
117
+ }
118
+ function isPendingPayload(operation, payload) {
119
+ if (!isRecord(payload)) {
120
+ return false;
121
+ }
122
+ const keys = Object.keys(payload);
123
+ if (operation === 'updateRecoverySecret') {
124
+ return (keys.every((key) => key === 'recoverySecret') &&
125
+ isHexSecret(payload.recoverySecret));
126
+ }
127
+ if (!Array.isArray(payload.identifiers) ||
128
+ !payload.identifiers.every(isIdentifier) ||
129
+ countDistinctIdentifiers(payload.identifiers) < MIN_IDENTIFIERS) {
130
+ return false;
131
+ }
132
+ if (operation === 'register') {
133
+ return (isHexSecret(payload.recoverySecret) &&
134
+ keys.every((key) => key === 'identifiers' || key === 'recoverySecret'));
135
+ }
136
+ return keys.every((key) => key === 'identifiers');
137
+ }
138
+ function isHexSecret(value) {
139
+ return typeof value === 'string' && /^0x(?:[0-9a-fA-F]{2})+$/u.test(value);
140
+ }
141
+ function isIdentifier(value) {
142
+ return (isRecord(value) &&
143
+ typeof value.type === 'string' &&
144
+ typeof value.namespace === 'string' &&
145
+ typeof value.value === 'string' &&
146
+ Object.prototype.hasOwnProperty.call(value, 'verifier'));
147
+ }
148
+ function isAuthControllerToken(value) {
149
+ if (!isRecord(value)) {
150
+ return false;
151
+ }
152
+ return (typeof value.profileId === 'string' &&
153
+ typeof value.requestHash === 'string' &&
154
+ (value.twoFactor === undefined || value.twoFactor === true) &&
155
+ (value.identifiersHash === undefined ||
156
+ typeof value.identifiersHash === 'string') &&
157
+ (value.identifierOwnershipApproved === undefined ||
158
+ value.identifierOwnershipApproved === true) &&
159
+ typeof value.issuer === 'string' &&
160
+ typeof value.expiresAt === 'number' &&
161
+ Number.isInteger(value.expiresAt) &&
162
+ value.expiresAt >= 0 &&
163
+ typeof value.signature === 'string');
164
+ }
165
+ function isMutationReceiptArray(value) {
166
+ if (!Array.isArray(value)) {
167
+ return false;
168
+ }
169
+ const escrowIds = new Set();
170
+ for (const receipt of value) {
171
+ if (!isMutationReceipt(receipt) || escrowIds.has(receipt.escrowId)) {
172
+ return false;
173
+ }
174
+ escrowIds.add(receipt.escrowId);
175
+ }
176
+ return true;
177
+ }
178
+ function throwInvalidPendingOperation() {
179
+ throw new MfaRecoveryError('Invalid pending operation', 'invalid_pending_operation');
180
+ }
181
+ //# sourceMappingURL=pending-operation-validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-operation-validation.js","sourceRoot":"","sources":["../src/pending-operation-validation.ts"],"names":[],"mappings":"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","sourcesContent":["import { hash } from './crypto.js';\nimport { MfaRecoveryError } from './errors.js';\nimport { isMutationReceipt } from './escrow-utils.js';\nimport {\n countDistinctIdentifiers,\n getIdentifierAuthMode,\n MIN_IDENTIFIERS,\n} from './identifier-auth.js';\nimport { assertSameAudienceIds } from './state-machine.js';\nimport type {\n AuthControllerToken,\n Identifier,\n Mutation,\n MutationReceipt,\n PendingMutationPayload,\n PendingRegisterPayload,\n PendingUpdateIdentifiersPayload,\n PendingUpdateRecoverySecretPayload,\n} from './types.js';\n\n/**\n * Validates decrypted pending state before it can trigger authorization or\n * escrow operations.\n *\n * @param pending - Decrypted pending state.\n * @param escrowIds - Build-configured escrow ids.\n * @throws If the pending state is malformed or inconsistent.\n */\nexport async function assertValidPendingOperation(\n pending: unknown,\n escrowIds: string[],\n): Promise<void> {\n if (!isRecord(pending)) {\n throwInvalidPendingOperation();\n }\n if (pending.phase !== 'authorizing' && pending.phase !== 'writing') {\n throwInvalidPendingOperation();\n }\n if (!isRecord(pending.mutation)) {\n throwInvalidPendingOperation();\n }\n\n const mutationValue = pending.mutation;\n if (\n typeof mutationValue.id !== 'string' ||\n typeof mutationValue.profileId !== 'string' ||\n !isMutationOperation(mutationValue.operation) ||\n typeof mutationValue.expectedVersion !== 'number' ||\n !Number.isInteger(mutationValue.expectedVersion) ||\n mutationValue.expectedVersion < 0 ||\n typeof mutationValue.newVersion !== 'number' ||\n !Number.isInteger(mutationValue.newVersion) ||\n mutationValue.newVersion < 0 ||\n mutationValue.newVersion !== mutationValue.expectedVersion + 1 ||\n typeof mutationValue.payloadHash !== 'string' ||\n typeof mutationValue.requestHash !== 'string' ||\n !Array.isArray(mutationValue.audiences) ||\n !mutationValue.audiences.every((audience): audience is string => {\n return typeof audience === 'string';\n })\n ) {\n throwInvalidPendingOperation();\n }\n const mutation = mutationValue as unknown as Mutation;\n assertSameAudienceIds(mutation.audiences, escrowIds);\n\n const { payload } = pending;\n if (!isPendingPayload(mutation.operation, payload)) {\n throwInvalidPendingOperation();\n }\n if (pendingPayloadHasIdentifiers(payload)) {\n for (const identifier of payload.identifiers) {\n getIdentifierAuthMode(identifier.type);\n }\n }\n\n if (\n mutation.operation === 'register'\n ? pending.identifier !== null\n : !isIdentifier(pending.identifier)\n ) {\n throwInvalidPendingOperation();\n }\n if (mutation.operation !== 'register' && isIdentifier(pending.identifier)) {\n getIdentifierAuthMode(pending.identifier.type);\n }\n\n if (mutation.operation === 'register' && mutation.expectedVersion !== 0) {\n throwInvalidPendingOperation();\n }\n if (hash(payload) !== mutation.payloadHash) {\n throw new MfaRecoveryError(\n 'Pending payload does not match mutation',\n 'payload_mismatch',\n );\n }\n const requestHash = hash({\n id: mutation.id,\n profileId: mutation.profileId,\n operation: mutation.operation,\n expectedVersion: mutation.expectedVersion,\n newVersion: mutation.newVersion,\n payloadHash: mutation.payloadHash,\n audiences: mutation.audiences,\n });\n if (requestHash !== mutation.requestHash) {\n throwInvalidPendingOperation();\n }\n\n if (pending.phase === 'authorizing') {\n return;\n }\n if (\n !isAuthControllerToken(pending.authControllerToken) ||\n pending.authControllerToken.profileId !== mutation.profileId ||\n pending.authControllerToken.requestHash !== mutation.requestHash ||\n (mutation.operation !== 'register' &&\n pending.authControllerToken.twoFactor !== true) ||\n ((mutation.operation === 'register' ||\n mutation.operation === 'updateIdentifiers') &&\n (pending.authControllerToken.identifierOwnershipApproved !== true ||\n typeof pending.authControllerToken.identifiersHash !== 'string')) ||\n !isMutationReceiptArray(pending.receipts)\n ) {\n throwInvalidPendingOperation();\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction isMutationOperation(value: unknown): value is Mutation['operation'] {\n return (\n value === 'register' ||\n value === 'updateRecoverySecret' ||\n value === 'updateIdentifiers'\n );\n}\n\n/**\n * @param payload - Logical pending mutation payload.\n * @returns Whether the payload includes an identifier set.\n */\nexport function pendingPayloadHasIdentifiers(\n payload: PendingMutationPayload,\n): payload is PendingRegisterPayload | PendingUpdateIdentifiersPayload {\n return Object.prototype.hasOwnProperty.call(payload, 'identifiers');\n}\n\n/**\n * @param payload - Logical pending mutation payload.\n * @returns Whether the payload includes a hex recovery secret.\n */\nexport function pendingPayloadHasRecoverySecret(\n payload: PendingMutationPayload,\n): payload is PendingRegisterPayload | PendingUpdateRecoverySecretPayload {\n return Object.prototype.hasOwnProperty.call(payload, 'recoverySecret');\n}\n\nfunction isPendingPayload(\n operation: Mutation['operation'],\n payload: unknown,\n): payload is PendingMutationPayload {\n if (!isRecord(payload)) {\n return false;\n }\n const keys = Object.keys(payload);\n if (operation === 'updateRecoverySecret') {\n return (\n keys.every((key) => key === 'recoverySecret') &&\n isHexSecret(payload.recoverySecret)\n );\n }\n if (\n !Array.isArray(payload.identifiers) ||\n !payload.identifiers.every(isIdentifier) ||\n countDistinctIdentifiers(payload.identifiers) < MIN_IDENTIFIERS\n ) {\n return false;\n }\n if (operation === 'register') {\n return (\n isHexSecret(payload.recoverySecret) &&\n keys.every((key) => key === 'identifiers' || key === 'recoverySecret')\n );\n }\n return keys.every((key) => key === 'identifiers');\n}\n\nfunction isHexSecret(value: unknown): value is string {\n return typeof value === 'string' && /^0x(?:[0-9a-fA-F]{2})+$/u.test(value);\n}\n\nfunction isIdentifier(value: unknown): value is Identifier {\n return (\n isRecord(value) &&\n typeof value.type === 'string' &&\n typeof value.namespace === 'string' &&\n typeof value.value === 'string' &&\n Object.prototype.hasOwnProperty.call(value, 'verifier')\n );\n}\n\nfunction isAuthControllerToken(value: unknown): value is AuthControllerToken {\n if (!isRecord(value)) {\n return false;\n }\n return (\n typeof value.profileId === 'string' &&\n typeof value.requestHash === 'string' &&\n (value.twoFactor === undefined || value.twoFactor === true) &&\n (value.identifiersHash === undefined ||\n typeof value.identifiersHash === 'string') &&\n (value.identifierOwnershipApproved === undefined ||\n value.identifierOwnershipApproved === true) &&\n typeof value.issuer === 'string' &&\n typeof value.expiresAt === 'number' &&\n Number.isInteger(value.expiresAt) &&\n value.expiresAt >= 0 &&\n typeof value.signature === 'string'\n );\n}\n\nfunction isMutationReceiptArray(value: unknown): value is MutationReceipt[] {\n if (!Array.isArray(value)) {\n return false;\n }\n const escrowIds = new Set<string>();\n for (const receipt of value) {\n if (!isMutationReceipt(receipt) || escrowIds.has(receipt.escrowId)) {\n return false;\n }\n escrowIds.add(receipt.escrowId);\n }\n return true;\n}\n\nfunction throwInvalidPendingOperation(): never {\n throw new MfaRecoveryError(\n 'Invalid pending operation',\n 'invalid_pending_operation',\n );\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import type { PendingOperation, RecoveryPhase } from './types.js';
2
+ /**
3
+ * Returns the persisted recovery phase.
4
+ *
5
+ * @param pending - Loaded pending operation, if any.
6
+ * @returns Current phase.
7
+ */
8
+ export declare function getRecoveryPhase(pending: PendingOperation | null): RecoveryPhase;
9
+ /**
10
+ * Abort is allowed while idle or authorizing. `writing` must `resume()`.
11
+ *
12
+ * @param pending - Loaded pending operation, if any.
13
+ * @throws If a mutation is already writing.
14
+ */
15
+ export declare function assertAbortAllowed(pending: PendingOperation | null): void;
16
+ /**
17
+ * Ensures a persisted mutation targets the build-configured escrow set.
18
+ *
19
+ * @param audiences - Mutation audience ids.
20
+ * @param escrowIds - Configured escrow ids.
21
+ */
22
+ export declare function assertSameAudienceIds(audiences: string[], escrowIds: string[]): void;
23
+ //# sourceMappingURL=state-machine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-machine.d.ts","sourceRoot":"","sources":["../src/state-machine.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAElE;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAC/B,aAAa,CAEf;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,IAAI,CAOzE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,EAAE,EACnB,SAAS,EAAE,MAAM,EAAE,GAClB,IAAI,CAUN"}
@@ -0,0 +1,34 @@
1
+ import { MfaRecoveryError } from './errors.js';
2
+ /**
3
+ * Returns the persisted recovery phase.
4
+ *
5
+ * @param pending - Loaded pending operation, if any.
6
+ * @returns Current phase.
7
+ */
8
+ export function getRecoveryPhase(pending) {
9
+ return pending?.phase ?? 'idle';
10
+ }
11
+ /**
12
+ * Abort is allowed while idle or authorizing. `writing` must `resume()`.
13
+ *
14
+ * @param pending - Loaded pending operation, if any.
15
+ * @throws If a mutation is already writing.
16
+ */
17
+ export function assertAbortAllowed(pending) {
18
+ if (pending?.phase === 'writing') {
19
+ throw new MfaRecoveryError('Cannot abort a mutation that may have been applied', 'abort_not_allowed');
20
+ }
21
+ }
22
+ /**
23
+ * Ensures a persisted mutation targets the build-configured escrow set.
24
+ *
25
+ * @param audiences - Mutation audience ids.
26
+ * @param escrowIds - Configured escrow ids.
27
+ */
28
+ export function assertSameAudienceIds(audiences, escrowIds) {
29
+ if (audiences.length !== escrowIds.length ||
30
+ audiences.some((id, index) => id !== escrowIds[index])) {
31
+ throw new MfaRecoveryError('Persisted mutation audiences do not match configured escrows', 'audience_mismatch');
32
+ }
33
+ }
34
+ //# sourceMappingURL=state-machine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-machine.js","sourceRoot":"","sources":["../src/state-machine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/C;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAgC;IAEhC,OAAO,OAAO,EAAE,KAAK,IAAI,MAAM,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAgC;IACjE,IAAI,OAAO,EAAE,KAAK,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,gBAAgB,CACxB,oDAAoD,EACpD,mBAAmB,CACpB,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,SAAmB,EACnB,SAAmB;IAEnB,IACE,SAAS,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM;QACrC,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,KAAK,SAAS,CAAC,KAAK,CAAC,CAAC,EACtD,CAAC;QACD,MAAM,IAAI,gBAAgB,CACxB,8DAA8D,EAC9D,mBAAmB,CACpB,CAAC;IACJ,CAAC;AACH,CAAC","sourcesContent":["import { MfaRecoveryError } from './errors.js';\nimport type { PendingOperation, RecoveryPhase } from './types.js';\n\n/**\n * Returns the persisted recovery phase.\n *\n * @param pending - Loaded pending operation, if any.\n * @returns Current phase.\n */\nexport function getRecoveryPhase(\n pending: PendingOperation | null,\n): RecoveryPhase {\n return pending?.phase ?? 'idle';\n}\n\n/**\n * Abort is allowed while idle or authorizing. `writing` must `resume()`.\n *\n * @param pending - Loaded pending operation, if any.\n * @throws If a mutation is already writing.\n */\nexport function assertAbortAllowed(pending: PendingOperation | null): void {\n if (pending?.phase === 'writing') {\n throw new MfaRecoveryError(\n 'Cannot abort a mutation that may have been applied',\n 'abort_not_allowed',\n );\n }\n}\n\n/**\n * Ensures a persisted mutation targets the build-configured escrow set.\n *\n * @param audiences - Mutation audience ids.\n * @param escrowIds - Configured escrow ids.\n */\nexport function assertSameAudienceIds(\n audiences: string[],\n escrowIds: string[],\n): void {\n if (\n audiences.length !== escrowIds.length ||\n audiences.some((id, index) => id !== escrowIds[index])\n ) {\n throw new MfaRecoveryError(\n 'Persisted mutation audiences do not match configured escrows',\n 'audience_mismatch',\n );\n }\n}\n"]}