{"version":3,"file":"email-policy.mjs","names":[],"sources":["../../src/internal/email-policy.ts"],"sourcesContent":["import {\n  type EmailAddressMutation,\n  type EmailAddressTarget,\n  type EmailAction,\n  EmailUnavailable,\n  snapshotEmailCredential,\n  snapshotEmailRequirement,\n  snapshotEmailRevision,\n} from \"@yielded/auth/Email\";\nimport type { LoginIdentifier } from \"@yielded/auth/Identity\";\nimport {\n  snapshotAuthenticationEvidence,\n  type AuthenticationRevision,\n  type AuthenticationRequirement,\n  type SecurityRevision,\n} from \"@yielded/auth/Sessions\";\nimport * as DateTime from \"effect/DateTime\";\nimport * as Effect from \"effect/Effect\";\n\nimport { PersistenceMappingError } from \"./mapping-error\";\nimport type { AnyEmailAddressMapping } from \"./models/email-model\";\nimport { passwordEvidenceSatisfiedAt } from \"./password-policy\";\n\nconst unavailable = () => EmailUnavailable.make({});\n\nconst sameIdentifier = (left: LoginIdentifier, right: LoginIdentifier) =>\n  left.namespace === right.namespace && left.value === right.value;\n\nexport type EmailWorkflowPolicy = Pick<\n  AnyEmailAddressMapping,\n  | \"allocateCredentialId\"\n  | \"allocateCredentialIdSync\"\n  | \"allocateRevision\"\n  | \"allocateRevisionSync\"\n  | \"sessionInvalidation\"\n  | \"isIdentifierConflict\"\n  | \"isCredentialConflict\"\n> & {\n  readonly subject: Pick<\n    AnyEmailAddressMapping[\"subject\"],\n    \"nextSecurityRevision\" | \"nextSecurityRevisionSync\"\n  >;\n};\n\nexport const sameEmailRevision = (left: AuthenticationRevision, right: AuthenticationRevision) => {\n  if (\n    left.subjectId !== right.subjectId ||\n    left.securityRevision !== right.securityRevision ||\n    left.credentials.length !== right.credentials.length\n  )\n    return false;\n\n  const expected = [...left.credentials].sort((a, b) =>\n    a.credentialId.localeCompare(b.credentialId),\n  );\n\n  const actual = [...right.credentials].sort((a, b) =>\n    a.credentialId.localeCompare(b.credentialId),\n  );\n\n  return expected.every(\n    (item, index) =>\n      item.credentialId === actual[index]!.credentialId &&\n      item.revision === actual[index]!.revision,\n  );\n};\n\nexport const emailActionModule = (moduleId: string, action: EmailAction) =>\n  `${moduleId}/${action === \"verify-address\" ? \"verify-address\" : \"change-address\"}`;\n\nexport const emailActionPurpose = (action: EmailAction) =>\n  action === \"verify-address\" ? \"email-address-verification\" : \"email-address-change\";\n\nexport const emailRedemptionMatches = (input: EmailAddressMutation, action: EmailAction) => {\n  const completion = input.redemption.input;\n  const binding = completion.binding;\n\n  return (\n    completion.moduleId === emailActionModule(input.moduleId, action) &&\n    completion.purpose === emailActionPurpose(action) &&\n    binding._tag === \"IdentifierChange\" &&\n    sameIdentifier(binding.identifier, input.target) &&\n    sameEmailRevision(binding.revision, input.captured.revision)\n  );\n};\n\nexport const snapshotEmailMutation = Effect.fn(\"EmailAddressPersistence.snapshotMutation\")(\n  function* (input: EmailAddressMutation) {\n    const evidence = yield* snapshotAuthenticationEvidence(input.authorization.evidence).pipe(\n      Effect.mapError(unavailable),\n    );\n\n    const requirement = yield* snapshotEmailRequirement(input.authorization.requirement);\n\n    const source =\n      input.captured.source === undefined\n        ? undefined\n        : yield* snapshotEmailCredential(input.captured.source);\n\n    return Object.freeze({\n      ...input,\n      target: Object.freeze({ ...input.target }),\n      ...(input.invalidation === undefined\n        ? {}\n        : { invalidation: Object.freeze({ ...input.invalidation }) }),\n      captured: Object.freeze({\n        eligible: input.captured.eligible,\n        revision: snapshotEmailRevision(input.captured.revision),\n        ...(input.captured.targetIdentifierRevision === undefined\n          ? {}\n          : { targetIdentifierRevision: input.captured.targetIdentifierRevision }),\n        ...(source === undefined ? {} : { source }),\n      }),\n      authorization: Object.freeze({\n        challenge: Object.freeze({\n          ...input.authorization.challenge,\n          target: Object.freeze({ ...input.authorization.challenge.target }),\n          revision: snapshotEmailRevision(input.authorization.challenge.revision),\n        }),\n        evidence,\n        requirement,\n      }),\n    });\n  },\n);\n\nexport const validateEmailMutation = Effect.fn(\"EmailAddressPersistence.validateAuthority\")(\n  function* (\n    policy: EmailWorkflowPolicy,\n    input: EmailAddressMutation,\n    action: EmailAction,\n    current: Pick<EmailMutationRead, \"target\" | \"requirement\"> | undefined,\n    now: number,\n  ) {\n    const confirmsExisting =\n      action === \"verify-address\" && input.captured.targetIdentifierRevision !== undefined;\n\n    if (\n      !input.captured.eligible ||\n      !emailRedemptionMatches(input, action) ||\n      input.authorization.challenge.moduleId !== input.moduleId ||\n      input.authorization.challenge.action !== action ||\n      input.authorization.challenge.commandId !== input.commandId ||\n      !sameIdentifier(input.authorization.challenge.target, input.target) ||\n      input.authorization.challenge.sourceCredentialId !== input.captured.source?.credentialId ||\n      !sameEmailRevision(input.authorization.challenge.revision, input.captured.revision) ||\n      input.authorization.challenge.targetIdentifierRevision !==\n        input.captured.targetIdentifierRevision ||\n      (input.authorization.evidence.flowId as string) !== (input.commandId as string) ||\n      input.authorization.evidence.bindingDigest !== input.authorization.challenge.bindingDigest ||\n      !sameEmailRevision(input.authorization.evidence.revision, input.captured.revision) ||\n      (action === \"verify-address\" && input.captured.source !== undefined) ||\n      (action === \"change-address\" && input.captured.source === undefined) ||\n      confirmsExisting !== (input.invalidation === undefined) ||\n      (input.invalidation?.existingSessions === \"immediate\" &&\n        policy.sessionInvalidation !== \"same-authority-immediate\")\n    )\n      return false;\n\n    if (\n      current === undefined ||\n      !current.target.eligible ||\n      !sameEmailRevision(current.target.revision, input.captured.revision) ||\n      current.target.targetIdentifierRevision !== input.captured.targetIdentifierRevision ||\n      (input.captured.source !== undefined &&\n        (current.target.source === undefined ||\n          current.target.source.identifierRevision !== input.captured.source.identifierRevision ||\n          current.target.source.credentialRevision !== input.captured.source.credentialRevision ||\n          !sameIdentifier(current.target.source.identifier, input.captured.source.identifier)))\n    )\n      return false;\n\n    const currentRequirement = yield* current.requirement;\n\n    // Address custody retains strict future-proof rejection.\n    if (\n      input.authorization.evidence.proofs.some(\n        (proof) => DateTime.toEpochMillis(proof.verifiedAt) > now,\n      )\n    )\n      return false;\n\n    return (\n      (yield* passwordEvidenceSatisfiedAt(\n        input.authorization.evidence,\n        input.authorization.requirement,\n        now,\n      )) &&\n      (yield* passwordEvidenceSatisfiedAt(input.authorization.evidence, currentRequirement, now))\n    );\n  },\n);\n\nexport const allocateEmailValue = <A>(\n  mode: \"interactive\" | \"synchronous\",\n  asynchronous: Effect.Effect<A, PersistenceMappingError> | undefined,\n  synchronous: (() => A) | undefined,\n) => {\n  if (mode === \"synchronous\")\n    return synchronous === undefined\n      ? Effect.fail(unavailable())\n      : Effect.try({\n          try: synchronous,\n          catch: (cause) => PersistenceMappingError.make({ operation: \"mapping\", cause }),\n        });\n  if (asynchronous !== undefined) return asynchronous;\n\n  return synchronous === undefined\n    ? Effect.fail(unavailable())\n    : Effect.try({\n        try: synchronous,\n        catch: (cause) => PersistenceMappingError.make({ operation: \"mapping\", cause }),\n      });\n};\n\nexport const allocateEmailSecurityRevision = (\n  mapping: EmailWorkflowPolicy,\n  mode: \"interactive\" | \"synchronous\",\n  current: SecurityRevision,\n) => {\n  if (mode === \"synchronous\")\n    return mapping.subject.nextSecurityRevisionSync === undefined\n      ? Effect.fail(unavailable())\n      : Effect.try({\n          try: () => mapping.subject.nextSecurityRevisionSync!(current),\n          catch: (cause) => PersistenceMappingError.make({ operation: \"mapping\", cause }),\n        });\n  if (mapping.subject.nextSecurityRevision !== undefined)\n    return mapping.subject.nextSecurityRevision(current);\n  if (mapping.subject.nextSecurityRevisionSync !== undefined)\n    return Effect.try({\n      try: () => mapping.subject.nextSecurityRevisionSync!(current),\n      catch: (cause) => PersistenceMappingError.make({ operation: \"mapping\", cause }),\n    });\n\n  return Effect.fail(unavailable());\n};\n\nexport interface EmailMutationRead {\n  readonly target: EmailAddressTarget;\n  readonly requirement: Effect.Effect<\n    AuthenticationRequirement,\n    EmailUnavailable | PersistenceMappingError\n  >;\n}\n"],"mappings":"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"}